auth
auth/client.go
Introduced scopes. Created a Scope type and a scopeStore interface, along with the memstore methods for the scopeStore. This will allow applications to define access with granularity, so users can grant access to some data, not _all_ data. We're operating on the assumption that there won't be an unreasonable number of scopes defined, so there is no paging operation included for the ListScopes method. This is a decision that may have to be revisited in the future, depending on usecases.
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 errors := []error{}
116 if c.Secret == nil && c.OwnerID == nil && c.Name == nil && c.Logo == nil && c.Website == nil {
117 errors = append(errors, ErrEmptyChange)
118 return errors
119 }
120 if c.Name != nil && len(*c.Name) < 2 {
121 errors = append(errors, ErrClientNameTooShort)
122 }
123 if c.Name != nil && len(*c.Name) > 32 {
124 errors = append(errors, ErrClientNameTooLong)
125 }
126 if c.Logo != nil && *c.Logo != "" {
127 if len(*c.Logo) > 1024 {
128 errors = append(errors, ErrClientLogoTooLong)
129 }
130 u, err := url.Parse(*c.Logo)
131 if err != nil || !u.IsAbs() {
132 errors = append(errors, ErrClientLogoNotURL)
133 }
134 }
135 if c.Website != nil && *c.Website != "" {
136 if len(*c.Website) > 140 {
137 errors = append(errors, ErrClientWebsiteTooLong)
138 }
139 u, err := url.Parse(*c.Website)
140 if err != nil || !u.IsAbs() {
141 errors = append(errors, ErrClientWebsiteNotURL)
142 }
143 }
144 return errors
145 }
147 func getClientAuth(w http.ResponseWriter, r *http.Request, allowPublic bool) (uuid.ID, string, bool) {
148 enc := json.NewEncoder(w)
149 clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth()
150 if !fromAuthHeader {
151 clientIDStr = r.PostFormValue("client_id")
152 }
153 if clientIDStr == "" {
154 w.WriteHeader(http.StatusUnauthorized)
155 if fromAuthHeader {
156 w.Header().Set("WWW-Authenticate", "Basic")
157 }
158 renderJSONError(enc, "invalid_client")
159 return nil, "", false
160 }
161 if !allowPublic && !fromAuthHeader {
162 w.WriteHeader(http.StatusBadRequest)
163 renderJSONError(enc, "unauthorized_client")
164 return nil, "", false
165 }
166 clientID, err := uuid.Parse(clientIDStr)
167 if err != nil {
168 log.Println("Error decoding client ID:", err)
169 w.WriteHeader(http.StatusUnauthorized)
170 if fromAuthHeader {
171 w.Header().Set("WWW-Authenticate", "Basic")
172 }
173 renderJSONError(enc, "invalid_client")
174 return nil, "", false
175 }
176 return clientID, clientSecret, true
177 }
179 func verifyClient(w http.ResponseWriter, r *http.Request, allowPublic bool, context Context) (uuid.ID, bool) {
180 enc := json.NewEncoder(w)
181 clientID, clientSecret, ok := getClientAuth(w, r, allowPublic)
182 if !ok {
183 return nil, false
184 }
185 _, _, fromAuthHeader := r.BasicAuth()
186 client, err := context.GetClient(clientID)
187 if err == ErrClientNotFound {
188 w.WriteHeader(http.StatusUnauthorized)
189 if fromAuthHeader {
190 w.Header().Set("WWW-Authenticate", "Basic")
191 }
192 renderJSONError(enc, "invalid_client")
193 return nil, false
194 } else if err != nil {
195 w.WriteHeader(http.StatusInternalServerError)
196 renderJSONError(enc, "server_error")
197 return nil, false
198 }
199 if client.Secret != clientSecret { // it's important that any client deemed "public" is not issued a client secret.
200 w.WriteHeader(http.StatusUnauthorized)
201 if fromAuthHeader {
202 w.Header().Set("WWW-Authenticate", "Basic")
203 }
204 renderJSONError(enc, "invalid_client")
205 return nil, false
206 }
207 return clientID, true
208 }
210 // Endpoint represents a single URI that a Client
211 // controls. Users will be redirected to these URIs
212 // following successful authorization grants and
213 // exchanges for access tokens.
214 type Endpoint struct {
215 ID uuid.ID `json:"id,omitempty"`
216 ClientID uuid.ID `json:"client_id,omitempty"`
217 URI string `json:"uri,omitempty"`
218 NormalizedURI string `json:"-"`
219 Added time.Time `json:"added,omitempty"`
220 }
222 func normalizeURIString(in string) (string, error) {
223 n, err := purell.NormalizeURLString(in, normalizeFlags)
224 if err != nil {
225 log.Println(err)
226 return in, ErrEndpointURINotURL
227 }
228 return n, nil
229 }
231 func normalizeURI(in *url.URL) string {
232 return purell.NormalizeURL(in, normalizeFlags)
233 }
235 type sortedEndpoints []Endpoint
237 func (s sortedEndpoints) Len() int {
238 return len(s)
239 }
241 func (s sortedEndpoints) Less(i, j int) bool {
242 return s[i].Added.Before(s[j].Added)
243 }
245 func (s sortedEndpoints) Swap(i, j int) {
246 s[i], s[j] = s[j], s[i]
247 }
249 type clientStore interface {
250 getClient(id uuid.ID) (Client, error)
251 saveClient(client Client) error
252 updateClient(id uuid.ID, change ClientChange) error
253 deleteClient(id uuid.ID) error
254 listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error)
256 addEndpoints(client uuid.ID, endpoint []Endpoint) error
257 removeEndpoint(client, endpoint uuid.ID) error
258 checkEndpoint(client uuid.ID, endpoint string) (bool, error)
259 listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error)
260 countEndpoints(client uuid.ID) (int64, error)
261 }
263 func (m *memstore) getClient(id uuid.ID) (Client, error) {
264 m.clientLock.RLock()
265 defer m.clientLock.RUnlock()
266 c, ok := m.clients[id.String()]
267 if !ok {
268 return Client{}, ErrClientNotFound
269 }
270 return c, nil
271 }
273 func (m *memstore) saveClient(client Client) error {
274 m.clientLock.Lock()
275 defer m.clientLock.Unlock()
276 if _, ok := m.clients[client.ID.String()]; ok {
277 return ErrClientAlreadyExists
278 }
279 m.clients[client.ID.String()] = client
280 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID)
281 return nil
282 }
284 func (m *memstore) updateClient(id uuid.ID, change ClientChange) error {
285 m.clientLock.Lock()
286 defer m.clientLock.Unlock()
287 c, ok := m.clients[id.String()]
288 if !ok {
289 return ErrClientNotFound
290 }
291 c.ApplyChange(change)
292 m.clients[id.String()] = c
293 return nil
294 }
296 func (m *memstore) deleteClient(id uuid.ID) error {
297 client, err := m.getClient(id)
298 if err != nil {
299 return err
300 }
301 m.clientLock.Lock()
302 defer m.clientLock.Unlock()
303 delete(m.clients, id.String())
304 pos := -1
305 for p, item := range m.profileClientLookup[client.OwnerID.String()] {
306 if item.Equal(id) {
307 pos = p
308 break
309 }
310 }
311 if pos >= 0 {
312 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()][:pos], m.profileClientLookup[client.OwnerID.String()][pos+1:]...)
313 }
314 return nil
315 }
317 func (m *memstore) listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) {
318 ids := m.lookupClientsByProfileID(ownerID.String())
319 if len(ids) > num+offset {
320 ids = ids[offset : num+offset]
321 } else if len(ids) > offset {
322 ids = ids[offset:]
323 } else {
324 return []Client{}, nil
325 }
326 clients := []Client{}
327 for _, id := range ids {
328 client, err := m.getClient(id)
329 if err != nil {
330 return []Client{}, err
331 }
332 clients = append(clients, client)
333 }
334 return clients, nil
335 }
337 func (m *memstore) addEndpoints(client uuid.ID, endpoints []Endpoint) error {
338 m.endpointLock.Lock()
339 defer m.endpointLock.Unlock()
340 m.endpoints[client.String()] = append(m.endpoints[client.String()], endpoints...)
341 return nil
342 }
344 func (m *memstore) removeEndpoint(client, endpoint uuid.ID) error {
345 m.endpointLock.Lock()
346 defer m.endpointLock.Unlock()
347 pos := -1
348 for p, item := range m.endpoints[client.String()] {
349 if item.ID.Equal(endpoint) {
350 pos = p
351 break
352 }
353 }
354 if pos >= 0 {
355 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...)
356 }
357 return nil
358 }
360 func (m *memstore) checkEndpoint(client uuid.ID, endpoint string) (bool, error) {
361 m.endpointLock.RLock()
362 defer m.endpointLock.RUnlock()
363 for _, candidate := range m.endpoints[client.String()] {
364 if endpoint == candidate.NormalizedURI {
365 return true, nil
366 }
367 }
368 return false, nil
369 }
371 func (m *memstore) listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) {
372 m.endpointLock.RLock()
373 defer m.endpointLock.RUnlock()
374 return m.endpoints[client.String()], nil
375 }
377 func (m *memstore) countEndpoints(client uuid.ID) (int64, error) {
378 m.endpointLock.RLock()
379 defer m.endpointLock.RUnlock()
380 return int64(len(m.endpoints[client.String()])), nil
381 }
383 type newClientReq struct {
384 Name string `json:"name"`
385 Logo string `json:"logo"`
386 Website string `json:"website"`
387 Type string `json:"type"`
388 Endpoints []string `json:"endpoints"`
389 }
391 func RegisterClientHandlers(r *mux.Router, context Context) {
392 r.Handle("/clients", wrap(context, CreateClientHandler)).Methods("POST")
393 r.Handle("/clients", wrap(context, ListClientsHandler)).Methods("GET")
394 r.Handle("/clients/{id}", wrap(context, GetClientHandler)).Methods("GET")
395 r.Handle("/clients/{id}", wrap(context, UpdateClientHandler)).Methods("PATCH")
396 // 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?
397 // BUG(paddy): We need to implement a handler to add an endpoint to a client.
398 // BUG(paddy): We need to implement a handler to remove an endpoint from a client.
399 // BUG(paddy): We need to implement a handler to list endpoints.
400 }
402 func CreateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
403 errors := []requestError{}
404 username, password, ok := r.BasicAuth()
405 if !ok {
406 errors = append(errors, requestError{Slug: requestErrAccessDenied})
407 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
408 return
409 }
410 profile, err := authenticate(username, password, c)
411 if err != nil {
412 errors = append(errors, requestError{Slug: requestErrAccessDenied})
413 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
414 return
415 }
416 var req newClientReq
417 decoder := json.NewDecoder(r.Body)
418 err = decoder.Decode(&req)
419 if err != nil {
420 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
421 return
422 }
423 if req.Type == "" {
424 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/type"})
425 } else if req.Type != clientTypePublic && req.Type != clientTypeConfidential {
426 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/type"})
427 }
428 if req.Name == "" {
429 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/name"})
430 } else if len(req.Name) < minClientNameLen {
431 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"})
432 } else if len(req.Name) > maxClientNameLen {
433 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"})
434 }
435 if len(errors) > 0 {
436 encode(w, r, http.StatusBadRequest, response{Errors: errors})
437 return
438 }
439 client := Client{
440 ID: uuid.NewID(),
441 OwnerID: profile.ID,
442 Name: req.Name,
443 Logo: req.Logo,
444 Website: req.Website,
445 Type: req.Type,
446 }
447 if client.Type == clientTypeConfidential {
448 secret := make([]byte, 32)
449 _, err = rand.Read(secret)
450 if err != nil {
451 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
452 return
453 }
454 client.Secret = hex.EncodeToString(secret)
455 }
456 err = c.SaveClient(client)
457 if err != nil {
458 if err == ErrClientAlreadyExists {
459 errors = append(errors, requestError{Slug: requestErrConflict, Field: "/id"})
460 encode(w, r, http.StatusBadRequest, response{Errors: errors})
461 return
462 }
463 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
464 return
465 }
466 endpoints := []Endpoint{}
467 for pos, u := range req.Endpoints {
468 uri, err := url.Parse(u)
469 if err != nil {
470 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
471 continue
472 }
473 if !uri.IsAbs() {
474 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)})
475 continue
476 }
477 endpoint := Endpoint{
478 ID: uuid.NewID(),
479 ClientID: client.ID,
480 URI: uri.String(),
481 Added: time.Now(),
482 }
483 endpoints = append(endpoints, endpoint)
484 }
485 err = c.AddEndpoints(client.ID, endpoints)
486 if err != nil {
487 errors = append(errors, requestError{Slug: requestErrActOfGod})
488 encode(w, r, http.StatusInternalServerError, response{Errors: errors, Clients: []Client{client}})
489 return
490 }
491 resp := response{
492 Clients: []Client{client},
493 Endpoints: endpoints,
494 Errors: errors,
495 }
496 encode(w, r, http.StatusCreated, resp)
497 }
499 func GetClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
500 errors := []requestError{}
501 vars := mux.Vars(r)
502 if vars["id"] == "" {
503 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
504 encode(w, r, http.StatusBadRequest, response{Errors: errors})
505 return
506 }
507 id, err := uuid.Parse(vars["id"])
508 if err != nil {
509 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
510 encode(w, r, http.StatusBadRequest, response{Errors: errors})
511 return
512 }
513 client, err := c.GetClient(id)
514 if err != nil {
515 if err == ErrClientNotFound {
516 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
517 encode(w, r, http.StatusBadRequest, response{Errors: errors})
518 return
519 }
520 errors = append(errors, requestError{Slug: requestErrActOfGod})
521 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
522 return
523 }
524 client.Secret = ""
525 // BUG(paddy): How should auth be handled for retrieving clients?
526 resp := response{
527 Clients: []Client{client},
528 Errors: errors,
529 }
530 encode(w, r, http.StatusOK, resp)
531 }
533 func ListClientsHandler(w http.ResponseWriter, r *http.Request, c Context) {
534 errors := []requestError{}
535 var err error
536 // BUG(paddy): If ids are provided in query params, retrieve only those clients
537 // BUG(paddy): We should have auth when listing clients
538 num := defaultClientResponseSize
539 offset := 0
540 ownerIDStr := r.URL.Query().Get("owner_id")
541 numStr := r.URL.Query().Get("num")
542 offsetStr := r.URL.Query().Get("offset")
543 if numStr != "" {
544 num, err = strconv.Atoi(numStr)
545 if err != nil {
546 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"})
547 }
548 if num > maxClientResponseSize {
549 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"})
550 }
551 }
552 if offsetStr != "" {
553 offset, err = strconv.Atoi(offsetStr)
554 if err != nil {
555 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"})
556 }
557 }
558 if ownerIDStr == "" {
559 errors = append(errors, requestError{Slug: requestErrMissing, Param: "owner_id"})
560 }
561 if len(errors) > 0 {
562 encode(w, r, http.StatusBadRequest, response{Errors: errors})
563 return
564 }
565 ownerID, err := uuid.Parse(ownerIDStr)
566 if err != nil {
567 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "owner_id"})
568 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
569 return
570 }
571 clients, err := c.ListClientsByOwner(ownerID, num, offset)
572 if err != nil {
573 errors = append(errors, requestError{Slug: requestErrActOfGod})
574 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
575 return
576 }
577 for pos, client := range clients {
578 client.Secret = ""
579 clients[pos] = client
580 }
581 resp := response{
582 Clients: clients,
583 Errors: errors,
584 }
585 encode(w, r, http.StatusOK, resp)
586 }
588 func UpdateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
589 errors := []requestError{}
590 vars := mux.Vars(r)
591 if _, ok := vars["id"]; !ok {
592 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
593 encode(w, r, http.StatusBadRequest, response{Errors: errors})
594 return
595 }
596 var change ClientChange
597 err := decode(r, &change)
598 if err != nil {
599 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/"})
600 encode(w, r, http.StatusBadRequest, response{Errors: errors})
601 return
602 }
603 errs := change.Validate()
604 for _, err := range errs {
605 switch err {
606 case ErrEmptyChange:
607 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/"})
608 case ErrClientNameTooShort:
609 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"})
610 case ErrClientNameTooLong:
611 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"})
612 case ErrClientLogoTooLong:
613 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/logo"})
614 case ErrClientLogoNotURL:
615 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/logo"})
616 case ErrClientWebsiteTooLong:
617 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/website"})
618 case ErrClientWebsiteNotURL:
619 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/website"})
620 default:
621 log.Println("Unrecognised error from client change validation:", err)
622 }
623 }
624 id, err := uuid.Parse(vars["id"])
625 if err != nil {
626 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
627 }
628 if len(errors) > 0 {
629 encode(w, r, http.StatusBadRequest, response{Errors: errors})
630 return
631 }
632 client, err := c.GetClient(id)
633 if err == ErrClientNotFound {
634 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
635 encode(w, r, http.StatusNotFound, response{Errors: errors})
636 return
637 } else if err != nil {
638 log.Println("Error retrieving client:", err)
639 errors = append(errors, requestError{Slug: requestErrActOfGod})
640 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
641 return
642 }
643 if change.Secret != nil && client.Type == clientTypeConfidential {
644 secret := make([]byte, 32)
645 _, err = rand.Read(secret)
646 if err != nil {
647 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
648 return
649 }
650 newSecret := hex.EncodeToString(secret)
651 change.Secret = &newSecret
652 }
653 err = c.UpdateClient(id, change)
654 if err != nil {
655 log.Println("Error updating client:", err)
656 errors = append(errors, requestError{Slug: requestErrActOfGod})
657 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
658 return
659 }
660 client.ApplyChange(change)
661 encode(w, r, http.StatusOK, response{Clients: []Client{client}, Errors: errors})
662 return
663 }
665 func clientCredentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool) {
666 scope = r.PostFormValue("scope")
667 valid = true
668 return
669 }
671 func clientCredentialsAuditString(r *http.Request) string {
672 return "client_credentials"
673 }