auth
auth/client.go
Add Client updating from the API. Add a handler to update Clients using the API. Add a helper that will decode a request for us based on its Content-Type header. Change the ClientChange.Validate function to return as many errors as possible, as opposed to just the first error it encounters. Update the ClientChange.Validate tests to take advantage of the new signature.
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 }