auth

Paddy 2015-01-28 Parent:f474ce964dcf Child:bc842183181d

132:163ce22fa4c9 Go to Latest

auth/client.go

Enable CSRF protection, add expiration to sessions. Sessions gain a CSRF token, which is passed as a parameter to the login page. The login page now checks for that CSRF token, and logs a CSRF attempt if the token does not match. I also added an expiration to sessions, so they don't last forever. Sessions should be pretty short--we just need to stay logged in for long enough to approve the OAuth request. Everything after that should be cookie based. Finally, I added a configuration parameter to control whether the session cookie should be set to Secure, requiring the use of HTTPS. For production use, this flag is a requirement, but it makes testing extremely difficult, so we need a way to disable it.

History
1 package auth
3 import (
4 "crypto/rand"
5 "encoding/hex"
6 "encoding/json"
7 "errors"
8 "log"
9 "net/http"
10 "net/url"
11 "strconv"
12 "time"
14 "github.com/PuerkitoBio/purell"
15 "github.com/gorilla/mux"
17 "code.secondbit.org/uuid.hg"
18 )
20 func init() {
21 RegisterGrantType("client_credentials", GrantType{
22 Validate: clientCredentialsValidate,
23 Invalidate: nil,
24 IssuesRefresh: true,
25 ReturnToken: RenderJSONToken,
26 AllowsPublic: false,
27 AuditString: clientCredentialsAuditString,
28 })
29 }
31 var (
32 // ErrNoClientStore is returned when a Context tries to act on a clientStore without setting one first.
33 ErrNoClientStore = errors.New("no clientStore was specified for the Context")
34 // ErrClientNotFound is returned when a Client is requested but not found in a clientStore.
35 ErrClientNotFound = errors.New("client not found in clientStore")
36 // ErrClientAlreadyExists is returned when a Client is added to a clientStore, but another Client with
37 // the same ID already exists in the clientStore.
38 ErrClientAlreadyExists = errors.New("client already exists in clientStore")
40 // ErrEmptyChange is returned when a Change has all its properties set to nil.
41 ErrEmptyChange = errors.New("change must have at least one property set")
42 // ErrClientNameTooShort is returned when a Client's Name property is too short.
43 ErrClientNameTooShort = errors.New("client name must be at least 2 characters")
44 // ErrClientNameTooLong is returned when a Client's Name property is too long.
45 ErrClientNameTooLong = errors.New("client name must be at most 32 characters")
46 // ErrClientLogoTooLong is returned when a Client's Logo property is too long.
47 ErrClientLogoTooLong = errors.New("client logo must be at most 1024 characters")
48 // ErrClientLogoNotURL is returned when a Client's Logo property is not a valid absolute URL.
49 ErrClientLogoNotURL = errors.New("client logo must be a valid absolute URL")
50 // ErrClientWebsiteTooLong is returned when a Client's Website property is too long.
51 ErrClientWebsiteTooLong = errors.New("client website must be at most 1024 characters")
52 // ErrClientWebsiteNotURL is returned when a Client's Website property is not a valid absolute URL.
53 ErrClientWebsiteNotURL = errors.New("client website must be a valid absolute URL")
54 // ErrEndpointURINotURL is returned when an Endpoint's URI property is not a valid absolute URL.
55 ErrEndpointURINotURL = errors.New("endpoint URI must be a valid absolute URL")
56 )
58 const (
59 clientTypePublic = "public"
60 clientTypeConfidential = "confidential"
61 minClientNameLen = 2
62 maxClientNameLen = 24
63 defaultClientResponseSize = 20
64 maxClientResponseSize = 50
66 normalizeFlags = purell.FlagsUsuallySafeNonGreedy | purell.FlagSortQuery
67 )
69 // Client represents a client that grants access
70 // to the auth server, exchanging grants for tokens,
71 // and tokens for access.
72 type Client struct {
73 ID uuid.ID `json:"id,omitempty"`
74 Secret string `json:"secret,omitempty"`
75 OwnerID uuid.ID `json:"owner_id,omitempty"`
76 Name string `json:"name,omitempty"`
77 Logo string `json:"logo,omitempty"`
78 Website string `json:"website,omitempty"`
79 Type string `json:"type,omitempty"`
80 }
82 // ApplyChange applies the properties of the passed
83 // ClientChange to the Client object it is called on.
84 func (c *Client) ApplyChange(change ClientChange) {
85 if change.Secret != nil {
86 c.Secret = *change.Secret
87 }
88 if change.OwnerID != nil {
89 c.OwnerID = change.OwnerID
90 }
91 if change.Name != nil {
92 c.Name = *change.Name
93 }
94 if change.Logo != nil {
95 c.Logo = *change.Logo
96 }
97 if change.Website != nil {
98 c.Website = *change.Website
99 }
100 }
102 // ClientChange represents a bundle of options for
103 // updating a Client's mutable data.
104 type ClientChange struct {
105 Secret *string
106 OwnerID uuid.ID
107 Name *string
108 Logo *string
109 Website *string
110 }
112 // Validate checks the ClientChange it is called on
113 // and asserts its internal validity, or lack thereof.
114 func (c ClientChange) Validate() error {
115 if c.Secret == nil && c.OwnerID == nil && c.Name == nil && c.Logo == nil && c.Website == nil {
116 return ErrEmptyChange
117 }
118 if c.Name != nil && len(*c.Name) < 2 {
119 return ErrClientNameTooShort
120 }
121 if c.Name != nil && len(*c.Name) > 32 {
122 return ErrClientNameTooLong
123 }
124 if c.Logo != nil && *c.Logo != "" {
125 if len(*c.Logo) > 1024 {
126 return ErrClientLogoTooLong
127 }
128 u, err := url.Parse(*c.Logo)
129 if err != nil || !u.IsAbs() {
130 return ErrClientLogoNotURL
131 }
132 }
133 if c.Website != nil && *c.Website != "" {
134 if len(*c.Website) > 140 {
135 return ErrClientWebsiteTooLong
136 }
137 u, err := url.Parse(*c.Website)
138 if err != nil || !u.IsAbs() {
139 return ErrClientWebsiteNotURL
140 }
141 }
142 return nil
143 }
145 func getClientAuth(w http.ResponseWriter, r *http.Request, allowPublic bool) (uuid.ID, string, bool) {
146 enc := json.NewEncoder(w)
147 clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth()
148 if !fromAuthHeader {
149 clientIDStr = r.PostFormValue("client_id")
150 }
151 if clientIDStr == "" {
152 w.WriteHeader(http.StatusUnauthorized)
153 if fromAuthHeader {
154 w.Header().Set("WWW-Authenticate", "Basic")
155 }
156 renderJSONError(enc, "invalid_client")
157 return nil, "", false
158 }
159 if !allowPublic && !fromAuthHeader {
160 w.WriteHeader(http.StatusBadRequest)
161 renderJSONError(enc, "unauthorized_client")
162 return nil, "", false
163 }
164 clientID, err := uuid.Parse(clientIDStr)
165 if err != nil {
166 log.Println("Error decoding client ID:", err)
167 w.WriteHeader(http.StatusUnauthorized)
168 if fromAuthHeader {
169 w.Header().Set("WWW-Authenticate", "Basic")
170 }
171 renderJSONError(enc, "invalid_client")
172 return nil, "", false
173 }
174 return clientID, clientSecret, true
175 }
177 func verifyClient(w http.ResponseWriter, r *http.Request, allowPublic bool, context Context) (uuid.ID, bool) {
178 enc := json.NewEncoder(w)
179 clientID, clientSecret, ok := getClientAuth(w, r, allowPublic)
180 if !ok {
181 return nil, false
182 }
183 _, _, fromAuthHeader := r.BasicAuth()
184 client, err := context.GetClient(clientID)
185 if err == ErrClientNotFound {
186 w.WriteHeader(http.StatusUnauthorized)
187 if fromAuthHeader {
188 w.Header().Set("WWW-Authenticate", "Basic")
189 }
190 renderJSONError(enc, "invalid_client")
191 return nil, false
192 } else if err != nil {
193 w.WriteHeader(http.StatusInternalServerError)
194 renderJSONError(enc, "server_error")
195 return nil, false
196 }
197 if client.Secret != clientSecret { // it's important that any client deemed "public" is not issued a client secret.
198 w.WriteHeader(http.StatusUnauthorized)
199 if fromAuthHeader {
200 w.Header().Set("WWW-Authenticate", "Basic")
201 }
202 renderJSONError(enc, "invalid_client")
203 return nil, false
204 }
205 return clientID, true
206 }
208 // Endpoint represents a single URI that a Client
209 // controls. Users will be redirected to these URIs
210 // following successful authorization grants and
211 // exchanges for access tokens.
212 type Endpoint struct {
213 ID uuid.ID `json:"id,omitempty"`
214 ClientID uuid.ID `json:"client_id,omitempty"`
215 URI string `json:"uri,omitempty"`
216 NormalizedURI string `json:"-"`
217 Added time.Time `json:"added,omitempty"`
218 }
220 func normalizeURIString(in string) (string, error) {
221 n, err := purell.NormalizeURLString(in, normalizeFlags)
222 if err != nil {
223 log.Println(err)
224 return in, ErrEndpointURINotURL
225 }
226 return n, nil
227 }
229 func normalizeURI(in *url.URL) string {
230 return purell.NormalizeURL(in, normalizeFlags)
231 }
233 type sortedEndpoints []Endpoint
235 func (s sortedEndpoints) Len() int {
236 return len(s)
237 }
239 func (s sortedEndpoints) Less(i, j int) bool {
240 return s[i].Added.Before(s[j].Added)
241 }
243 func (s sortedEndpoints) Swap(i, j int) {
244 s[i], s[j] = s[j], s[i]
245 }
247 type clientStore interface {
248 getClient(id uuid.ID) (Client, error)
249 saveClient(client Client) error
250 updateClient(id uuid.ID, change ClientChange) error
251 deleteClient(id uuid.ID) error
252 listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error)
254 addEndpoints(client uuid.ID, endpoint []Endpoint) error
255 removeEndpoint(client, endpoint uuid.ID) error
256 checkEndpoint(client uuid.ID, endpoint string) (bool, error)
257 listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error)
258 countEndpoints(client uuid.ID) (int64, error)
259 }
261 func (m *memstore) getClient(id uuid.ID) (Client, error) {
262 m.clientLock.RLock()
263 defer m.clientLock.RUnlock()
264 c, ok := m.clients[id.String()]
265 if !ok {
266 return Client{}, ErrClientNotFound
267 }
268 return c, nil
269 }
271 func (m *memstore) saveClient(client Client) error {
272 m.clientLock.Lock()
273 defer m.clientLock.Unlock()
274 if _, ok := m.clients[client.ID.String()]; ok {
275 return ErrClientAlreadyExists
276 }
277 m.clients[client.ID.String()] = client
278 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID)
279 return nil
280 }
282 func (m *memstore) updateClient(id uuid.ID, change ClientChange) error {
283 m.clientLock.Lock()
284 defer m.clientLock.Unlock()
285 c, ok := m.clients[id.String()]
286 if !ok {
287 return ErrClientNotFound
288 }
289 c.ApplyChange(change)
290 m.clients[id.String()] = c
291 return nil
292 }
294 func (m *memstore) deleteClient(id uuid.ID) error {
295 client, err := m.getClient(id)
296 if err != nil {
297 return err
298 }
299 m.clientLock.Lock()
300 defer m.clientLock.Unlock()
301 delete(m.clients, id.String())
302 pos := -1
303 for p, item := range m.profileClientLookup[client.OwnerID.String()] {
304 if item.Equal(id) {
305 pos = p
306 break
307 }
308 }
309 if pos >= 0 {
310 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()][:pos], m.profileClientLookup[client.OwnerID.String()][pos+1:]...)
311 }
312 return nil
313 }
315 func (m *memstore) listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) {
316 ids := m.lookupClientsByProfileID(ownerID.String())
317 if len(ids) > num+offset {
318 ids = ids[offset : num+offset]
319 } else if len(ids) > offset {
320 ids = ids[offset:]
321 } else {
322 return []Client{}, nil
323 }
324 clients := []Client{}
325 for _, id := range ids {
326 client, err := m.getClient(id)
327 if err != nil {
328 return []Client{}, err
329 }
330 clients = append(clients, client)
331 }
332 return clients, nil
333 }
335 func (m *memstore) addEndpoints(client uuid.ID, endpoints []Endpoint) error {
336 m.endpointLock.Lock()
337 defer m.endpointLock.Unlock()
338 m.endpoints[client.String()] = append(m.endpoints[client.String()], endpoints...)
339 return nil
340 }
342 func (m *memstore) removeEndpoint(client, endpoint uuid.ID) error {
343 m.endpointLock.Lock()
344 defer m.endpointLock.Unlock()
345 pos := -1
346 for p, item := range m.endpoints[client.String()] {
347 if item.ID.Equal(endpoint) {
348 pos = p
349 break
350 }
351 }
352 if pos >= 0 {
353 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...)
354 }
355 return nil
356 }
358 func (m *memstore) checkEndpoint(client uuid.ID, endpoint string) (bool, error) {
359 m.endpointLock.RLock()
360 defer m.endpointLock.RUnlock()
361 for _, candidate := range m.endpoints[client.String()] {
362 if endpoint == candidate.NormalizedURI {
363 return true, nil
364 }
365 }
366 return false, nil
367 }
369 func (m *memstore) listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) {
370 m.endpointLock.RLock()
371 defer m.endpointLock.RUnlock()
372 return m.endpoints[client.String()], nil
373 }
375 func (m *memstore) countEndpoints(client uuid.ID) (int64, error) {
376 m.endpointLock.RLock()
377 defer m.endpointLock.RUnlock()
378 return int64(len(m.endpoints[client.String()])), nil
379 }
381 type newClientReq struct {
382 Name string `json:"name"`
383 Logo string `json:"logo"`
384 Website string `json:"website"`
385 Type string `json:"type"`
386 Endpoints []string `json:"endpoints"`
387 }
389 func RegisterClientHandlers(r *mux.Router, context Context) {
390 r.Handle("/clients", wrap(context, CreateClientHandler)).Methods("POST")
391 r.Handle("/clients", wrap(context, ListClientsHandler)).Methods("GET")
392 r.Handle("/clients/{id}", wrap(context, GetClientHandler)).Methods("GET")
393 // BUG(paddy): We need to implement a handler to update a client.
394 // BUG(paddy): We need to implement a handler to delete a client. Also, what should that do with the grants and tokens belonging to that client?
395 // BUG(paddy): We need to implement a handler to add an endpoint to a client.
396 // BUG(paddy): We need to implement a handler to remove an endpoint from a client.
397 // BUG(paddy): We need to implement a handler to list endpoints.
398 }
400 func CreateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
401 errors := []requestError{}
402 username, password, ok := r.BasicAuth()
403 if !ok {
404 errors = append(errors, requestError{Slug: requestErrAccessDenied})
405 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
406 return
407 }
408 profile, err := authenticate(username, password, c)
409 if err != nil {
410 errors = append(errors, requestError{Slug: requestErrAccessDenied})
411 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
412 return
413 }
414 var req newClientReq
415 decoder := json.NewDecoder(r.Body)
416 err = decoder.Decode(&req)
417 if err != nil {
418 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
419 return
420 }
421 if req.Type == "" {
422 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/type"})
423 } else if req.Type != clientTypePublic && req.Type != clientTypeConfidential {
424 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/type"})
425 }
426 if req.Name == "" {
427 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/name"})
428 } else if len(req.Name) < minClientNameLen {
429 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"})
430 } else if len(req.Name) > maxClientNameLen {
431 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"})
432 }
433 if len(errors) > 0 {
434 encode(w, r, http.StatusBadRequest, response{Errors: errors})
435 return
436 }
437 client := Client{
438 ID: uuid.NewID(),
439 OwnerID: profile.ID,
440 Name: req.Name,
441 Logo: req.Logo,
442 Website: req.Website,
443 Type: req.Type,
444 }
445 if client.Type == clientTypeConfidential {
446 secret := make([]byte, 32)
447 _, err = rand.Read(secret)
448 if err != nil {
449 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
450 return
451 }
452 client.Secret = hex.EncodeToString(secret)
453 }
454 err = c.SaveClient(client)
455 if err != nil {
456 if err == ErrClientAlreadyExists {
457 errors = append(errors, requestError{Slug: requestErrConflict, Field: "/id"})
458 encode(w, r, http.StatusBadRequest, response{Errors: errors})
459 return
460 }
461 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
462 return
463 }
464 endpoints := []Endpoint{}
465 for pos, u := range req.Endpoints {
466 uri, err := url.Parse(u)
467 if err != nil {
468 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
469 continue
470 }
471 if !uri.IsAbs() {
472 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)})
473 continue
474 }
475 endpoint := Endpoint{
476 ID: uuid.NewID(),
477 ClientID: client.ID,
478 URI: uri.String(),
479 Added: time.Now(),
480 }
481 endpoints = append(endpoints, endpoint)
482 }
483 err = c.AddEndpoints(client.ID, endpoints)
484 if err != nil {
485 errors = append(errors, requestError{Slug: requestErrActOfGod})
486 encode(w, r, http.StatusInternalServerError, response{Errors: errors, Clients: []Client{client}})
487 return
488 }
489 resp := response{
490 Clients: []Client{client},
491 Endpoints: endpoints,
492 Errors: errors,
493 }
494 encode(w, r, http.StatusCreated, resp)
495 }
497 func GetClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
498 errors := []requestError{}
499 vars := mux.Vars(r)
500 if vars["id"] == "" {
501 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
502 encode(w, r, http.StatusBadRequest, response{Errors: errors})
503 return
504 }
505 id, err := uuid.Parse(vars["id"])
506 if err != nil {
507 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
508 encode(w, r, http.StatusBadRequest, response{Errors: errors})
509 return
510 }
511 client, err := c.GetClient(id)
512 if err != nil {
513 if err == ErrClientNotFound {
514 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
515 encode(w, r, http.StatusBadRequest, response{Errors: errors})
516 return
517 }
518 errors = append(errors, requestError{Slug: requestErrActOfGod})
519 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
520 return
521 }
522 client.Secret = ""
523 // BUG(paddy): How should auth be handled for retrieving clients?
524 resp := response{
525 Clients: []Client{client},
526 Errors: errors,
527 }
528 encode(w, r, http.StatusOK, resp)
529 }
531 func ListClientsHandler(w http.ResponseWriter, r *http.Request, c Context) {
532 errors := []requestError{}
533 var err error
534 // BUG(paddy): If ids are provided in query params, retrieve only those clients
535 // BUG(paddy): We should have auth when listing clients
536 num := defaultClientResponseSize
537 offset := 0
538 ownerIDStr := r.URL.Query().Get("owner_id")
539 numStr := r.URL.Query().Get("num")
540 offsetStr := r.URL.Query().Get("offset")
541 if numStr != "" {
542 num, err = strconv.Atoi(numStr)
543 if err != nil {
544 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"})
545 }
546 if num > maxClientResponseSize {
547 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"})
548 }
549 }
550 if offsetStr != "" {
551 offset, err = strconv.Atoi(offsetStr)
552 if err != nil {
553 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"})
554 }
555 }
556 if ownerIDStr == "" {
557 errors = append(errors, requestError{Slug: requestErrMissing, Param: "owner_id"})
558 }
559 if len(errors) > 0 {
560 encode(w, r, http.StatusBadRequest, response{Errors: errors})
561 return
562 }
563 ownerID, err := uuid.Parse(ownerIDStr)
564 if err != nil {
565 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "owner_id"})
566 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
567 return
568 }
569 clients, err := c.ListClientsByOwner(ownerID, num, offset)
570 if err != nil {
571 errors = append(errors, requestError{Slug: requestErrActOfGod})
572 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
573 return
574 }
575 for pos, client := range clients {
576 client.Secret = ""
577 clients[pos] = client
578 }
579 resp := response{
580 Clients: clients,
581 Errors: errors,
582 }
583 encode(w, r, http.StatusOK, resp)
584 }
586 func clientCredentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool) {
587 scope = r.PostFormValue("scope")
588 valid = true
589 return
590 }
592 func clientCredentialsAuditString(r *http.Request) string {
593 return "client_credentials"
594 }