auth

Paddy 2015-03-03 Parent:bc842183181d Child:f59559b33c76

135:d30a3a12d387 Go to Latest

auth/client.go

Attach our Scope type to AuthCodes and Tokens. When obtaining an AuthorizationCode or Token, attach a slice of strings, each one a Scope ID, instead of just attaching the encoded string the user passes in. This will allow us to change our Scope encoding down the line, and is more conceptually faithful. Also, if an authorization request is made with an invalid scope, return the invalid_scope error.

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 "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 }