auth
auth/client.go
Implement handlers for retrieving clients. Create a GetClientHandler and ListClientsHandler for retrieving details about a client. Currently, we're not returning the client secret for these clients. We're also not doing any auth. We may want to restrict auth to the owner of the clients, and return secrets only when auth'd, and maybe even only when a special header is included. Alternatively, we could only return the secret when retrieving a single client. Still unsure how I want to handle that.
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 }