Test our GetClientHandler function, add isAuthError helper.
Add a helper that identifies whether the error passed to it is an authentication
error or is some other type of error. This is useful fo checking whether or not
an internal error occurred while authenticating users.
Update all instances where we call our authentication helper to make them use
the new error helper. All tests continue to pass.
Add a new test case for retrieving a client as an unauthenticated user. This
clears the client's secret from the response before sending it.
Update the GetClientHandler function to return the secret when the owner of the
client used Basic Auth in the request.
Add a new test case for retrieving a client as an authenticated user, both the
owner and a non-owner user. This makes sure the secret is divulged only in the
appropriate cases.
15 "github.com/PuerkitoBio/purell"
16 "github.com/gorilla/mux"
18 "code.secondbit.org/uuid.hg"
22 RegisterGrantType("client_credentials", GrantType{
23 Validate: clientCredentialsValidate,
26 ReturnToken: RenderJSONToken,
28 AuditString: clientCredentialsAuditString,
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")
60 clientTypePublic = "public"
61 clientTypeConfidential = "confidential"
64 defaultClientResponseSize = 20
65 maxClientResponseSize = 50
66 defaultEndpointResponseSize = 20
67 maxEndpointResponseSize = 50
69 normalizeFlags = purell.FlagsUsuallySafeNonGreedy | purell.FlagSortQuery
72 // Client represents a client that grants access
73 // to the auth server, exchanging grants for tokens,
74 // and tokens for access.
76 ID uuid.ID `json:"id,omitempty"`
77 Secret string `json:"secret,omitempty"`
78 OwnerID uuid.ID `json:"owner_id,omitempty"`
79 Name string `json:"name,omitempty"`
80 Logo string `json:"logo,omitempty"`
81 Website string `json:"website,omitempty"`
82 Type string `json:"type,omitempty"`
85 // ApplyChange applies the properties of the passed
86 // ClientChange to the Client object it is called on.
87 func (c *Client) ApplyChange(change ClientChange) {
88 if change.Secret != nil {
89 c.Secret = *change.Secret
91 if change.OwnerID != nil {
92 c.OwnerID = change.OwnerID
94 if change.Name != nil {
97 if change.Logo != nil {
100 if change.Website != nil {
101 c.Website = *change.Website
105 // ClientChange represents a bundle of options for
106 // updating a Client's mutable data.
107 type ClientChange struct {
115 // Validate checks the ClientChange it is called on
116 // and asserts its internal validity, or lack thereof.
117 func (c ClientChange) Validate() []error {
119 if c.Secret == nil && c.OwnerID == nil && c.Name == nil && c.Logo == nil && c.Website == nil {
120 errors = append(errors, ErrEmptyChange)
123 if c.Name != nil && len(*c.Name) < 2 {
124 errors = append(errors, ErrClientNameTooShort)
126 if c.Name != nil && len(*c.Name) > 32 {
127 errors = append(errors, ErrClientNameTooLong)
129 if c.Logo != nil && *c.Logo != "" {
130 if len(*c.Logo) > 1024 {
131 errors = append(errors, ErrClientLogoTooLong)
133 u, err := url.Parse(*c.Logo)
134 if err != nil || !u.IsAbs() {
135 errors = append(errors, ErrClientLogoNotURL)
138 if c.Website != nil && *c.Website != "" {
139 if len(*c.Website) > 140 {
140 errors = append(errors, ErrClientWebsiteTooLong)
142 u, err := url.Parse(*c.Website)
143 if err != nil || !u.IsAbs() {
144 errors = append(errors, ErrClientWebsiteNotURL)
150 func getClientAuth(w http.ResponseWriter, r *http.Request, allowPublic bool) (uuid.ID, string, bool) {
151 enc := json.NewEncoder(w)
152 clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth()
154 clientIDStr = r.PostFormValue("client_id")
156 if clientIDStr == "" {
157 w.WriteHeader(http.StatusUnauthorized)
159 w.Header().Set("WWW-Authenticate", "Basic")
161 renderJSONError(enc, "invalid_client")
162 return nil, "", false
164 if !allowPublic && !fromAuthHeader {
165 w.WriteHeader(http.StatusBadRequest)
166 renderJSONError(enc, "unauthorized_client")
167 return nil, "", false
169 clientID, err := uuid.Parse(clientIDStr)
171 log.Println("Error decoding client ID:", err)
172 w.WriteHeader(http.StatusUnauthorized)
174 w.Header().Set("WWW-Authenticate", "Basic")
176 renderJSONError(enc, "invalid_client")
177 return nil, "", false
179 return clientID, clientSecret, true
182 func verifyClient(w http.ResponseWriter, r *http.Request, allowPublic bool, context Context) (uuid.ID, bool) {
183 enc := json.NewEncoder(w)
184 clientID, clientSecret, ok := getClientAuth(w, r, allowPublic)
188 _, _, fromAuthHeader := r.BasicAuth()
189 client, err := context.GetClient(clientID)
190 if err == ErrClientNotFound {
191 w.WriteHeader(http.StatusUnauthorized)
193 w.Header().Set("WWW-Authenticate", "Basic")
195 renderJSONError(enc, "invalid_client")
197 } else if err != nil {
198 w.WriteHeader(http.StatusInternalServerError)
199 renderJSONError(enc, "server_error")
202 if client.Secret != clientSecret { // it's important that any client deemed "public" is not issued a client secret.
203 w.WriteHeader(http.StatusUnauthorized)
205 w.Header().Set("WWW-Authenticate", "Basic")
207 renderJSONError(enc, "invalid_client")
210 return clientID, true
213 // Endpoint represents a single URI that a Client
214 // controls. Users will be redirected to these URIs
215 // following successful authorization grants and
216 // exchanges for access tokens.
217 type Endpoint struct {
218 ID uuid.ID `json:"id,omitempty"`
219 ClientID uuid.ID `json:"client_id,omitempty"`
220 URI string `json:"uri,omitempty"`
221 NormalizedURI string `json:"-"`
222 Added time.Time `json:"added,omitempty"`
225 func normalizeURIString(in string) (string, error) {
226 n, err := purell.NormalizeURLString(in, normalizeFlags)
229 return in, ErrEndpointURINotURL
234 func normalizeURI(in *url.URL) string {
235 return purell.NormalizeURL(in, normalizeFlags)
238 type sortedEndpoints []Endpoint
240 func (s sortedEndpoints) Len() int {
244 func (s sortedEndpoints) Less(i, j int) bool {
245 return s[i].Added.Before(s[j].Added)
248 func (s sortedEndpoints) Swap(i, j int) {
249 s[i], s[j] = s[j], s[i]
252 type clientStore interface {
253 getClient(id uuid.ID) (Client, error)
254 saveClient(client Client) error
255 updateClient(id uuid.ID, change ClientChange) error
256 deleteClient(id uuid.ID) error
257 listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error)
259 addEndpoints(client uuid.ID, endpoint []Endpoint) error
260 removeEndpoint(client, endpoint uuid.ID) error
261 checkEndpoint(client uuid.ID, endpoint string) (bool, error)
262 listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error)
263 countEndpoints(client uuid.ID) (int64, error)
266 func (m *memstore) getClient(id uuid.ID) (Client, error) {
268 defer m.clientLock.RUnlock()
269 c, ok := m.clients[id.String()]
271 return Client{}, ErrClientNotFound
276 func (m *memstore) saveClient(client Client) error {
278 defer m.clientLock.Unlock()
279 if _, ok := m.clients[client.ID.String()]; ok {
280 return ErrClientAlreadyExists
282 m.clients[client.ID.String()] = client
283 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID)
287 func (m *memstore) updateClient(id uuid.ID, change ClientChange) error {
289 defer m.clientLock.Unlock()
290 c, ok := m.clients[id.String()]
292 return ErrClientNotFound
294 c.ApplyChange(change)
295 m.clients[id.String()] = c
299 func (m *memstore) deleteClient(id uuid.ID) error {
300 client, err := m.getClient(id)
305 defer m.clientLock.Unlock()
306 delete(m.clients, id.String())
308 for p, item := range m.profileClientLookup[client.OwnerID.String()] {
315 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()][:pos], m.profileClientLookup[client.OwnerID.String()][pos+1:]...)
320 func (m *memstore) listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) {
321 ids := m.lookupClientsByProfileID(ownerID.String())
322 if len(ids) > num+offset {
323 ids = ids[offset : num+offset]
324 } else if len(ids) > offset {
327 return []Client{}, nil
329 clients := []Client{}
330 for _, id := range ids {
331 client, err := m.getClient(id)
333 return []Client{}, err
335 clients = append(clients, client)
340 func (m *memstore) addEndpoints(client uuid.ID, endpoints []Endpoint) error {
341 m.endpointLock.Lock()
342 defer m.endpointLock.Unlock()
343 m.endpoints[client.String()] = append(m.endpoints[client.String()], endpoints...)
347 func (m *memstore) removeEndpoint(client, endpoint uuid.ID) error {
348 m.endpointLock.Lock()
349 defer m.endpointLock.Unlock()
351 for p, item := range m.endpoints[client.String()] {
352 if item.ID.Equal(endpoint) {
358 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...)
363 func (m *memstore) checkEndpoint(client uuid.ID, endpoint string) (bool, error) {
364 m.endpointLock.RLock()
365 defer m.endpointLock.RUnlock()
366 for _, candidate := range m.endpoints[client.String()] {
367 if endpoint == candidate.NormalizedURI {
374 func (m *memstore) listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) {
375 m.endpointLock.RLock()
376 defer m.endpointLock.RUnlock()
377 return m.endpoints[client.String()], nil
380 func (m *memstore) countEndpoints(client uuid.ID) (int64, error) {
381 m.endpointLock.RLock()
382 defer m.endpointLock.RUnlock()
383 return int64(len(m.endpoints[client.String()])), nil
386 type newClientReq struct {
387 Name string `json:"name"`
388 Logo string `json:"logo"`
389 Website string `json:"website"`
390 Type string `json:"type"`
391 Endpoints []string `json:"endpoints"`
394 func RegisterClientHandlers(r *mux.Router, context Context) {
395 r.Handle("/clients", wrap(context, CreateClientHandler)).Methods("POST")
396 r.Handle("/clients", wrap(context, ListClientsHandler)).Methods("GET")
397 r.Handle("/clients/{id}", wrap(context, GetClientHandler)).Methods("GET")
398 r.Handle("/clients/{id}", wrap(context, UpdateClientHandler)).Methods("PATCH")
399 // 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?
400 r.Handle("/clients/{id}/endpoints", wrap(context, AddEndpointsHandler)).Methods("POST")
401 // BUG(paddy): We need to implement a handler to remove an endpoint from a client.
402 r.Handle("/clients/{id}/endpoints", wrap(context, ListEndpointsHandler)).Methods("GET")
405 func CreateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
406 errors := []requestError{}
407 username, password, ok := r.BasicAuth()
409 errors = append(errors, requestError{Slug: requestErrAccessDenied})
410 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
413 profile, err := authenticate(username, password, c)
415 if isAuthError(err) {
416 errors = append(errors, requestError{Slug: requestErrAccessDenied})
417 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
419 errors = append(errors, requestError{Slug: requestErrActOfGod})
420 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
425 decoder := json.NewDecoder(r.Body)
426 err = decoder.Decode(&req)
428 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
432 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/type"})
433 } else if req.Type != clientTypePublic && req.Type != clientTypeConfidential {
434 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/type"})
437 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/name"})
438 } else if len(req.Name) < minClientNameLen {
439 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"})
440 } else if len(req.Name) > maxClientNameLen {
441 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"})
444 encode(w, r, http.StatusBadRequest, response{Errors: errors})
452 Website: req.Website,
455 if client.Type == clientTypeConfidential {
456 secret := make([]byte, 32)
457 _, err = rand.Read(secret)
459 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
462 client.Secret = hex.EncodeToString(secret)
464 err = c.SaveClient(client)
466 if err == ErrClientAlreadyExists {
467 errors = append(errors, requestError{Slug: requestErrConflict, Field: "/id"})
468 encode(w, r, http.StatusBadRequest, response{Errors: errors})
471 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
474 endpoints := []Endpoint{}
475 for pos, u := range req.Endpoints {
476 uri, err := url.Parse(u)
478 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
482 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)})
485 endpoint := Endpoint{
491 endpoints = append(endpoints, endpoint)
493 err = c.AddEndpoints(client.ID, endpoints)
495 errors = append(errors, requestError{Slug: requestErrActOfGod})
496 encode(w, r, http.StatusInternalServerError, response{Errors: errors, Clients: []Client{client}})
500 Clients: []Client{client},
501 Endpoints: endpoints,
504 encode(w, r, http.StatusCreated, resp)
507 func GetClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
508 errors := []requestError{}
510 if vars["id"] == "" {
511 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
512 encode(w, r, http.StatusBadRequest, response{Errors: errors})
515 id, err := uuid.Parse(vars["id"])
517 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
518 encode(w, r, http.StatusBadRequest, response{Errors: errors})
521 client, err := c.GetClient(id)
523 if err == ErrClientNotFound {
524 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
525 encode(w, r, http.StatusNotFound, response{Errors: errors})
528 errors = append(errors, requestError{Slug: requestErrActOfGod})
529 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
532 username, password, ok := r.BasicAuth()
536 profile, err := authenticate(username, password, c)
538 if isAuthError(err) {
539 errors = append(errors, requestError{Slug: requestErrAccessDenied})
540 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
542 errors = append(errors, requestError{Slug: requestErrActOfGod})
543 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
547 if !client.OwnerID.Equal(profile.ID) {
552 Clients: []Client{client},
555 encode(w, r, http.StatusOK, resp)
558 func ListClientsHandler(w http.ResponseWriter, r *http.Request, c Context) {
559 errors := []requestError{}
561 // BUG(paddy): If ids are provided in query params, retrieve only those clients
562 // BUG(paddy): We should have auth when listing clients
563 num := defaultClientResponseSize
565 ownerIDStr := r.URL.Query().Get("owner_id")
566 numStr := r.URL.Query().Get("num")
567 offsetStr := r.URL.Query().Get("offset")
569 num, err = strconv.Atoi(numStr)
571 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"})
573 if num > maxClientResponseSize {
574 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"})
578 offset, err = strconv.Atoi(offsetStr)
580 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"})
583 if ownerIDStr == "" {
584 errors = append(errors, requestError{Slug: requestErrMissing, Param: "owner_id"})
587 encode(w, r, http.StatusBadRequest, response{Errors: errors})
590 ownerID, err := uuid.Parse(ownerIDStr)
592 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "owner_id"})
593 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
596 clients, err := c.ListClientsByOwner(ownerID, num, offset)
598 errors = append(errors, requestError{Slug: requestErrActOfGod})
599 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
602 for pos, client := range clients {
604 clients[pos] = client
610 encode(w, r, http.StatusOK, resp)
613 func UpdateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
614 errors := []requestError{}
616 if _, ok := vars["id"]; !ok {
617 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
618 encode(w, r, http.StatusBadRequest, response{Errors: errors})
621 var change ClientChange
622 err := decode(r, &change)
624 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/"})
625 encode(w, r, http.StatusBadRequest, response{Errors: errors})
628 errs := change.Validate()
629 for _, err := range errs {
632 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/"})
633 case ErrClientNameTooShort:
634 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"})
635 case ErrClientNameTooLong:
636 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"})
637 case ErrClientLogoTooLong:
638 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/logo"})
639 case ErrClientLogoNotURL:
640 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/logo"})
641 case ErrClientWebsiteTooLong:
642 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/website"})
643 case ErrClientWebsiteNotURL:
644 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/website"})
646 log.Println("Unrecognised error from client change validation:", err)
649 id, err := uuid.Parse(vars["id"])
651 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
654 encode(w, r, http.StatusBadRequest, response{Errors: errors})
657 client, err := c.GetClient(id)
658 if err == ErrClientNotFound {
659 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
660 encode(w, r, http.StatusNotFound, response{Errors: errors})
662 } else if err != nil {
663 log.Println("Error retrieving client:", err)
664 errors = append(errors, requestError{Slug: requestErrActOfGod})
665 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
668 if change.Secret != nil && client.Type == clientTypeConfidential {
669 secret := make([]byte, 32)
670 _, err = rand.Read(secret)
672 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
675 newSecret := hex.EncodeToString(secret)
676 change.Secret = &newSecret
678 err = c.UpdateClient(id, change)
680 log.Println("Error updating client:", err)
681 errors = append(errors, requestError{Slug: requestErrActOfGod})
682 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
685 client.ApplyChange(change)
686 encode(w, r, http.StatusOK, response{Clients: []Client{client}, Errors: errors})
690 func AddEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) {
691 type addEndpointReq struct {
692 Endpoints []string `json:"endpoints"`
694 errors := []requestError{}
696 if vars["id"] == "" {
697 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
698 encode(w, r, http.StatusBadRequest, response{Errors: errors})
701 id, err := uuid.Parse(vars["id"])
703 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
704 encode(w, r, http.StatusBadRequest, response{Errors: errors})
707 _, err = c.GetClient(id)
709 if err == ErrClientNotFound {
710 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
711 encode(w, r, http.StatusBadRequest, response{Errors: errors})
714 errors = append(errors, requestError{Slug: requestErrActOfGod})
715 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
718 var req addEndpointReq
719 decoder := json.NewDecoder(r.Body)
720 err = decoder.Decode(&req)
722 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
725 if len(req.Endpoints) < 1 {
726 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/endpoints"})
727 encode(w, r, http.StatusBadRequest, response{Errors: errors})
730 endpoints := []Endpoint{}
731 for pos, u := range req.Endpoints {
732 if parsed, err := url.Parse(u); err != nil {
733 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
735 } else if !parsed.IsAbs() {
736 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints" + strconv.Itoa(pos)})
745 endpoints = append(endpoints, e)
748 encode(w, r, http.StatusBadRequest, response{Errors: errors})
751 err = c.AddEndpoints(id, endpoints)
753 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
758 Endpoints: endpoints,
760 encode(w, r, http.StatusCreated, resp)
763 func ListEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) {
764 errors := []requestError{}
766 clientID, err := uuid.Parse(vars["id"])
768 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "client_id"})
769 encode(w, r, http.StatusBadRequest, response{Errors: errors})
772 num := defaultEndpointResponseSize
774 numStr := r.URL.Query().Get("num")
775 offsetStr := r.URL.Query().Get("offset")
777 num, err = strconv.Atoi(numStr)
779 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"})
781 if num > maxEndpointResponseSize {
782 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"})
786 offset, err = strconv.Atoi(offsetStr)
788 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"})
792 encode(w, r, http.StatusBadRequest, response{Errors: errors})
795 endpoints, err := c.ListEndpoints(clientID, num, offset)
797 errors = append(errors, requestError{Slug: requestErrActOfGod})
798 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
802 Endpoints: endpoints,
805 encode(w, r, http.StatusOK, resp)
808 func clientCredentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool) {
809 scopes = strings.Split(r.PostFormValue("scope"), " ")
814 func clientCredentialsAuditString(r *http.Request) string {
815 return "client_credentials"