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