Switch to a JWT approach.
We're going to use a JWT as our access tokens (as discussed in &yet's excellent
post https://blog.andyet.com/2015/05/12/micro-services-user-info-and-auth and my
ensuing conversation with Fritzy).
The benefit of this approach is that we can do authentication and even some
authorization without touching the database at all.
The drawback is that we can no longer revoke access tokens, only the refresh
tokens that grant the access tokens.
We need a new config variable to set our private key, used to sign the JWT.
We get to remove our token handlers, as we no longer can revoke tokens, so
there's no purpose in getting information about it or listing them.
Our tokenStore revokeToken gets to be simplified, as it will only ever be used
for refresh tokens now. We also updated our postgres and memstore
implementations.
We added a helper method for generating the signed "access token" (our JWT) and
started using it in the places where we're creating a Token.
We get to remove the `revoked` SQL column for the tokens table, and rename the
`refresh_revoked` column to just be `revoked`.
We shortened our access token expiration to 15 minutes instead of an hour, to
deal with the token not being revokable.
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")
40 // ErrEndpointNotFound is returned when an Endpoint is requested but not found in a clientSTore.
41 ErrEndpointNotFound = errors.New("endpoint not found in clientStore")
42 // ErrEndpointAlreadyExists is returned when an Endpoint is added to a clientStore, but another Endpoint
43 // with the same ID already exists in the clientStore.
44 ErrEndpointAlreadyExists = errors.New("endpoint already exists in clientStore")
46 // ErrEmptyChange is returned when a Change has all its properties set to nil.
47 ErrEmptyChange = errors.New("change must have at least one property set")
48 // ErrClientNameTooShort is returned when a Client's Name property is too short.
49 ErrClientNameTooShort = errors.New("client name must be at least 2 characters")
50 // ErrClientNameTooLong is returned when a Client's Name property is too long.
51 ErrClientNameTooLong = errors.New("client name must be at most 32 characters")
52 // ErrClientLogoTooLong is returned when a Client's Logo property is too long.
53 ErrClientLogoTooLong = errors.New("client logo must be at most 1024 characters")
54 // ErrClientLogoNotURL is returned when a Client's Logo property is not a valid absolute URL.
55 ErrClientLogoNotURL = errors.New("client logo must be a valid absolute URL")
56 // ErrClientWebsiteTooLong is returned when a Client's Website property is too long.
57 ErrClientWebsiteTooLong = errors.New("client website must be at most 1024 characters")
58 // ErrClientWebsiteNotURL is returned when a Client's Website property is not a valid absolute URL.
59 ErrClientWebsiteNotURL = errors.New("client website must be a valid absolute URL")
60 // ErrEndpointURINotURL is returned when an Endpoint's URI property is not a valid absolute URL.
61 ErrEndpointURINotURL = errors.New("endpoint URI must be a valid absolute URL")
65 clientTypePublic = "public"
66 clientTypeConfidential = "confidential"
69 defaultClientResponseSize = 20
70 maxClientResponseSize = 50
71 defaultEndpointResponseSize = 20
72 maxEndpointResponseSize = 50
74 normalizeFlags = purell.FlagsUsuallySafeNonGreedy | purell.FlagSortQuery
77 // Client represents a client that grants access
78 // to the auth server, exchanging grants for tokens,
79 // and tokens for access.
81 ID uuid.ID `json:"id,omitempty"`
82 Secret string `json:"secret,omitempty"`
83 OwnerID uuid.ID `json:"owner_id,omitempty"`
84 Name string `json:"name,omitempty"`
85 Logo string `json:"logo,omitempty"`
86 Website string `json:"website,omitempty"`
87 Type string `json:"type,omitempty"`
88 Deleted bool `json:"deleted,omitempty"`
91 // ApplyChange applies the properties of the passed
92 // ClientChange to the Client object it is called on.
93 func (c *Client) ApplyChange(change ClientChange) {
94 if change.Secret != nil {
95 c.Secret = *change.Secret
97 if change.OwnerID != nil {
98 c.OwnerID = change.OwnerID
100 if change.Name != nil {
101 c.Name = *change.Name
103 if change.Logo != nil {
104 c.Logo = *change.Logo
106 if change.Website != nil {
107 c.Website = *change.Website
109 if change.Deleted != nil {
110 c.Deleted = *change.Deleted
114 // ClientChange represents a bundle of options for
115 // updating a Client's mutable data.
116 type ClientChange struct {
125 func (c ClientChange) Empty() bool {
126 return c.Secret == nil && c.OwnerID == nil && c.Name == nil && c.Logo == nil && c.Website == nil && c.Deleted == nil
129 // Validate checks the ClientChange it is called on
130 // and asserts its internal validity, or lack thereof.
131 func (c ClientChange) Validate() []error {
134 errors = append(errors, ErrEmptyChange)
137 if c.Name != nil && len(*c.Name) < 2 {
138 errors = append(errors, ErrClientNameTooShort)
140 if c.Name != nil && len(*c.Name) > 32 {
141 errors = append(errors, ErrClientNameTooLong)
143 if c.Logo != nil && *c.Logo != "" {
144 if len(*c.Logo) > 1024 {
145 errors = append(errors, ErrClientLogoTooLong)
147 u, err := url.Parse(*c.Logo)
148 if err != nil || !u.IsAbs() {
149 errors = append(errors, ErrClientLogoNotURL)
152 if c.Website != nil && *c.Website != "" {
153 if len(*c.Website) > 140 {
154 errors = append(errors, ErrClientWebsiteTooLong)
156 u, err := url.Parse(*c.Website)
157 if err != nil || !u.IsAbs() {
158 errors = append(errors, ErrClientWebsiteNotURL)
164 func getClientAuth(w http.ResponseWriter, r *http.Request, allowPublic bool) (uuid.ID, string, bool) {
165 enc := json.NewEncoder(w)
166 clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth()
168 clientIDStr = r.PostFormValue("client_id")
170 if clientIDStr == "" {
171 w.WriteHeader(http.StatusUnauthorized)
173 w.Header().Set("WWW-Authenticate", "Basic")
175 renderJSONError(enc, "invalid_client")
176 return nil, "", false
178 if !allowPublic && !fromAuthHeader {
179 w.WriteHeader(http.StatusBadRequest)
180 renderJSONError(enc, "unauthorized_client")
181 return nil, "", false
183 clientID, err := uuid.Parse(clientIDStr)
185 log.Println("Error decoding client ID:", err)
186 w.WriteHeader(http.StatusUnauthorized)
188 w.Header().Set("WWW-Authenticate", "Basic")
190 renderJSONError(enc, "invalid_client")
191 return nil, "", false
193 return clientID, clientSecret, true
196 func verifyClient(w http.ResponseWriter, r *http.Request, allowPublic bool, context Context) (uuid.ID, bool) {
197 enc := json.NewEncoder(w)
198 clientID, clientSecret, ok := getClientAuth(w, r, allowPublic)
202 _, _, fromAuthHeader := r.BasicAuth()
203 client, err := context.GetClient(clientID)
204 if err == ErrClientNotFound {
205 w.WriteHeader(http.StatusUnauthorized)
207 w.Header().Set("WWW-Authenticate", "Basic")
209 renderJSONError(enc, "invalid_client")
211 } else if err != nil {
212 w.WriteHeader(http.StatusInternalServerError)
213 renderJSONError(enc, "server_error")
216 if client.Secret != clientSecret { // it's important that any client deemed "public" is not issued a client secret.
217 w.WriteHeader(http.StatusUnauthorized)
219 w.Header().Set("WWW-Authenticate", "Basic")
221 renderJSONError(enc, "invalid_client")
224 return clientID, true
227 // Endpoint represents a single URI that a Client
228 // controls. Users will be redirected to these URIs
229 // following successful authorization grants and
230 // exchanges for access tokens.
231 type Endpoint struct {
232 ID uuid.ID `json:"id,omitempty"`
233 ClientID uuid.ID `json:"client_id,omitempty"`
234 URI string `json:"uri,omitempty"`
235 NormalizedURI string `json:"-"`
236 Added time.Time `json:"added,omitempty"`
239 func normalizeURIString(in string) (string, error) {
240 n, err := purell.NormalizeURLString(in, normalizeFlags)
243 return in, ErrEndpointURINotURL
248 func normalizeURI(in *url.URL) string {
249 return purell.NormalizeURL(in, normalizeFlags)
252 type sortedEndpoints []Endpoint
254 func (s sortedEndpoints) Len() int {
258 func (s sortedEndpoints) Less(i, j int) bool {
259 return s[i].Added.Before(s[j].Added)
262 func (s sortedEndpoints) Swap(i, j int) {
263 s[i], s[j] = s[j], s[i]
266 type clientStore interface {
267 getClient(id uuid.ID) (Client, error)
268 saveClient(client Client) error
269 updateClient(id uuid.ID, change ClientChange) error
270 listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error)
271 deleteClientsByOwner(ownerID uuid.ID) error
273 addEndpoints(endpoint []Endpoint) error
274 removeEndpoint(client, endpoint uuid.ID) error
275 getEndpoint(client, endpoint uuid.ID) (Endpoint, error)
276 checkEndpoint(client uuid.ID, endpoint string) (bool, error)
277 listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error)
278 removeEndpointsByClientID(client uuid.ID) error
279 countEndpoints(client uuid.ID) (int64, error)
282 func (m *memstore) getClient(id uuid.ID) (Client, error) {
284 defer m.clientLock.RUnlock()
285 c, ok := m.clients[id.String()]
286 if !ok || c.Deleted {
287 return Client{}, ErrClientNotFound
292 func (m *memstore) saveClient(client Client) error {
294 defer m.clientLock.Unlock()
295 if _, ok := m.clients[client.ID.String()]; ok {
296 return ErrClientAlreadyExists
298 m.clients[client.ID.String()] = client
299 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID)
303 func (m *memstore) updateClient(id uuid.ID, change ClientChange) error {
305 defer m.clientLock.Unlock()
306 c, ok := m.clients[id.String()]
308 return ErrClientNotFound
310 c.ApplyChange(change)
311 m.clients[id.String()] = c
315 func (m *memstore) listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) {
316 ids := m.lookupClientsByProfileID(ownerID.String())
317 if len(ids) > num+offset && num > 0 {
318 ids = ids[offset : num+offset]
319 } else if len(ids) > offset {
322 return []Client{}, nil
324 clients := []Client{}
325 for _, id := range ids {
326 client, err := m.getClient(id)
328 if err == ErrClientNotFound {
331 return []Client{}, err
333 clients = append(clients, client)
338 func (m *memstore) deleteClientsByOwner(ownerID uuid.ID) error {
339 ids := m.lookupClientsByProfileID(ownerID.String())
341 defer m.clientLock.RUnlock()
342 for _, id := range ids {
343 client, ok := m.clients[id.String()]
347 client.Deleted = true
348 m.clients[id.String()] = client
353 func (m *memstore) addEndpoints(endpoints []Endpoint) error {
354 m.endpointLock.Lock()
355 defer m.endpointLock.Unlock()
356 clients := map[string][]Endpoint{}
357 for _, endpoint := range endpoints {
358 clients[endpoint.ClientID.String()] = append(clients[endpoint.ClientID.String()], endpoint)
360 for client, e := range clients {
361 m.endpoints[client] = append(m.endpoints[client], e...)
366 func (m *memstore) getEndpoint(client, endpoint uuid.ID) (Endpoint, error) {
367 m.endpointLock.Lock()
368 defer m.endpointLock.Unlock()
369 for _, item := range m.endpoints[client.String()] {
370 if item.ID.Equal(endpoint) {
374 return Endpoint{}, ErrEndpointNotFound
377 func (m *memstore) removeEndpoint(client, endpoint uuid.ID) error {
378 m.endpointLock.Lock()
379 defer m.endpointLock.Unlock()
381 for p, item := range m.endpoints[client.String()] {
382 if item.ID.Equal(endpoint) {
388 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...)
393 func (m *memstore) checkEndpoint(client uuid.ID, endpoint string) (bool, error) {
394 m.endpointLock.RLock()
395 defer m.endpointLock.RUnlock()
396 for _, candidate := range m.endpoints[client.String()] {
397 if endpoint == candidate.NormalizedURI {
404 func (m *memstore) listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) {
405 m.endpointLock.RLock()
406 defer m.endpointLock.RUnlock()
407 return m.endpoints[client.String()], nil
410 func (m *memstore) removeEndpointsByClientID(client uuid.ID) error {
411 m.endpointLock.Lock()
412 defer m.endpointLock.Unlock()
413 delete(m.endpoints, client.String())
417 func (m *memstore) countEndpoints(client uuid.ID) (int64, error) {
418 m.endpointLock.RLock()
419 defer m.endpointLock.RUnlock()
420 return int64(len(m.endpoints[client.String()])), nil
423 func cleanUpAfterClientDeletion(client uuid.ID, context Context) {
424 err := context.RemoveEndpointsByClientID(client)
426 log.Printf("Error removing endpoints from client %s: %+v\n", client, err)
428 err = context.DeleteAuthorizationCodesByClientID(client)
430 log.Printf("Error removing auth codes belonging to client %s: %+v\n", client, err)
432 err = context.RevokeTokensByClientID(client)
434 log.Printf("Error revoking tokens belonging to client %s: %+v\n", client, err)
438 type newClientReq struct {
439 Name string `json:"name"`
440 Logo string `json:"logo"`
441 Website string `json:"website"`
442 Type string `json:"type"`
443 Endpoints []string `json:"endpoints"`
446 func RegisterClientHandlers(r *mux.Router, context Context) {
447 r.Handle("/clients", wrap(context, CreateClientHandler)).Methods("POST")
448 r.Handle("/clients", wrap(context, ListClientsHandler)).Methods("GET")
449 r.Handle("/clients/{id}", wrap(context, GetClientHandler)).Methods("GET")
450 r.Handle("/clients/{id}", wrap(context, UpdateClientHandler)).Methods("PATCH")
451 r.Handle("/clients/{id}", wrap(context, RemoveClientHandler)).Methods("DELETE")
452 r.Handle("/clients/{id}/endpoints", wrap(context, AddEndpointsHandler)).Methods("POST")
453 r.Handle("/clients/{client_id}/endpoints/{id}", wrap(context, RemoveEndpointHandler)).Methods("DELETE")
454 r.Handle("/clients/{id}/endpoints", wrap(context, ListEndpointsHandler)).Methods("GET")
457 func CreateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
458 errors := []requestError{}
459 username, password, ok := r.BasicAuth()
461 errors = append(errors, requestError{Slug: requestErrAccessDenied})
462 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
465 profile, err := authenticate(username, password, c)
467 if isAuthError(err) {
468 errors = append(errors, requestError{Slug: requestErrAccessDenied})
469 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
471 log.Printf("Error authenticating: %#+v\n", err)
472 errors = append(errors, requestError{Slug: requestErrActOfGod})
473 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
478 decoder := json.NewDecoder(r.Body)
479 err = decoder.Decode(&req)
481 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
485 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/type"})
486 } else if req.Type != clientTypePublic && req.Type != clientTypeConfidential {
487 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/type"})
490 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/name"})
491 } else if len(req.Name) < minClientNameLen {
492 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"})
493 } else if len(req.Name) > maxClientNameLen {
494 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"})
497 encode(w, r, http.StatusBadRequest, response{Errors: errors})
505 Website: req.Website,
508 if client.Type == clientTypeConfidential {
509 secret := make([]byte, 32)
510 _, err = rand.Read(secret)
512 log.Printf("Error generating secret: %#+v\n", err)
513 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
516 client.Secret = hex.EncodeToString(secret)
518 err = c.SaveClient(client)
520 if err == ErrClientAlreadyExists {
521 errors = append(errors, requestError{Slug: requestErrConflict, Field: "/id"})
522 encode(w, r, http.StatusBadRequest, response{Errors: errors})
525 log.Printf("Error saving client: %#+v\n", err)
526 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
529 endpoints := []Endpoint{}
530 for pos, u := range req.Endpoints {
531 uri, err := url.Parse(u)
533 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
537 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)})
540 endpoint := Endpoint{
546 endpoints = append(endpoints, endpoint)
548 err = c.AddEndpoints(endpoints)
550 log.Printf("Error adding endpoints: %#+v\n", err)
551 errors = append(errors, requestError{Slug: requestErrActOfGod})
552 encode(w, r, http.StatusInternalServerError, response{Errors: errors, Clients: []Client{client}})
556 Clients: []Client{client},
557 Endpoints: endpoints,
560 encode(w, r, http.StatusCreated, resp)
563 func GetClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
564 errors := []requestError{}
566 if vars["id"] == "" {
567 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
568 encode(w, r, http.StatusBadRequest, response{Errors: errors})
571 id, err := uuid.Parse(vars["id"])
573 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
574 encode(w, r, http.StatusBadRequest, response{Errors: errors})
577 client, err := c.GetClient(id)
579 if err == ErrClientNotFound {
580 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
581 encode(w, r, http.StatusNotFound, response{Errors: errors})
584 errors = append(errors, requestError{Slug: requestErrActOfGod})
585 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
588 username, password, ok := r.BasicAuth()
592 profile, err := authenticate(username, password, c)
594 if isAuthError(err) {
595 errors = append(errors, requestError{Slug: requestErrAccessDenied})
596 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
598 errors = append(errors, requestError{Slug: requestErrActOfGod})
599 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
603 if !client.OwnerID.Equal(profile.ID) {
608 Clients: []Client{client},
611 encode(w, r, http.StatusOK, resp)
614 func ListClientsHandler(w http.ResponseWriter, r *http.Request, c Context) {
615 errors := []requestError{}
617 // BUG(paddy): If ids are provided in query params, retrieve only those clients
618 num := defaultClientResponseSize
620 ownerIDStr := r.URL.Query().Get("owner_id")
621 numStr := r.URL.Query().Get("num")
622 offsetStr := r.URL.Query().Get("offset")
624 num, err = strconv.Atoi(numStr)
626 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"})
628 if num > maxClientResponseSize {
629 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"})
632 errors = append(errors, requestError{Slug: requestErrInsufficient, Param: "num"})
636 offset, err = strconv.Atoi(offsetStr)
638 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"})
641 if ownerIDStr == "" {
642 errors = append(errors, requestError{Slug: requestErrMissing, Param: "owner_id"})
645 encode(w, r, http.StatusBadRequest, response{Errors: errors})
648 ownerID, err := uuid.Parse(ownerIDStr)
650 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "owner_id"})
651 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
654 clients, err := c.ListClientsByOwner(ownerID, num, offset)
656 errors = append(errors, requestError{Slug: requestErrActOfGod})
657 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
660 username, password, ok := r.BasicAuth()
662 for pos, client := range clients {
664 clients[pos] = client
667 profile, err := authenticate(username, password, c)
669 if isAuthError(err) {
670 errors = append(errors, requestError{Slug: requestErrAccessDenied})
671 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
673 errors = append(errors, requestError{Slug: requestErrActOfGod})
674 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
678 for pos, client := range clients {
679 if !client.OwnerID.Equal(profile.ID) {
681 clients[pos] = client
689 encode(w, r, http.StatusOK, resp)
692 func UpdateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
693 errors := []requestError{}
695 if _, ok := vars["id"]; !ok {
696 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
697 encode(w, r, http.StatusBadRequest, response{Errors: errors})
700 id, err := uuid.Parse(vars["id"])
702 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
704 username, password, ok := r.BasicAuth()
706 errors = append(errors, requestError{Slug: requestErrAccessDenied})
707 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
710 profile, err := authenticate(username, password, c)
712 if isAuthError(err) {
713 errors = append(errors, requestError{Slug: requestErrAccessDenied})
714 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
716 errors = append(errors, requestError{Slug: requestErrActOfGod})
717 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
721 var change ClientChange
722 err = decode(r, &change)
724 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/"})
725 encode(w, r, http.StatusBadRequest, response{Errors: errors})
728 errs := change.Validate()
729 for _, err := range errs {
732 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/"})
733 case ErrClientNameTooShort:
734 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"})
735 case ErrClientNameTooLong:
736 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"})
737 case ErrClientLogoTooLong:
738 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/logo"})
739 case ErrClientLogoNotURL:
740 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/logo"})
741 case ErrClientWebsiteTooLong:
742 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/website"})
743 case ErrClientWebsiteNotURL:
744 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/website"})
746 log.Println("Unrecognised error from client change validation:", err)
750 encode(w, r, http.StatusBadRequest, response{Errors: errors})
753 client, err := c.GetClient(id)
754 if err == ErrClientNotFound {
755 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
756 encode(w, r, http.StatusNotFound, response{Errors: errors})
758 } else if err != nil {
759 log.Println("Error retrieving client:", err)
760 errors = append(errors, requestError{Slug: requestErrActOfGod})
761 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
764 if !client.OwnerID.Equal(profile.ID) {
765 errors = append(errors, requestError{Slug: requestErrAccessDenied})
766 encode(w, r, http.StatusForbidden, response{Errors: errors})
769 if change.Secret != nil && client.Type == clientTypeConfidential {
770 secret := make([]byte, 32)
771 _, err = rand.Read(secret)
773 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
776 newSecret := hex.EncodeToString(secret)
777 change.Secret = &newSecret
779 err = c.UpdateClient(id, change)
781 log.Println("Error updating client:", err)
782 errors = append(errors, requestError{Slug: requestErrActOfGod})
783 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
786 client.ApplyChange(change)
787 encode(w, r, http.StatusOK, response{Clients: []Client{client}, Errors: errors})
791 func RemoveClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
792 errors := []requestError{}
794 if _, ok := vars["id"]; !ok {
795 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
796 encode(w, r, http.StatusNotFound, response{Errors: errors})
799 id, err := uuid.Parse(vars["id"])
801 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
803 username, password, ok := r.BasicAuth()
805 errors = append(errors, requestError{Slug: requestErrAccessDenied})
806 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
809 profile, err := authenticate(username, password, c)
811 if isAuthError(err) {
812 errors = append(errors, requestError{Slug: requestErrAccessDenied})
813 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
815 errors = append(errors, requestError{Slug: requestErrActOfGod})
816 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
820 client, err := c.GetClient(id)
822 if err == ErrClientNotFound {
823 errors = append(errors, requestError{Slug: requestErrNotFound})
824 encode(w, r, http.StatusNotFound, response{Errors: errors})
827 log.Println("Error retrieving client:", err)
828 errors = append(errors, requestError{Slug: requestErrActOfGod})
829 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
832 if !client.OwnerID.Equal(profile.ID) {
833 errors = append(errors, requestError{Slug: requestErrAccessDenied})
834 encode(w, r, http.StatusForbidden, response{Errors: errors})
838 change := ClientChange{Deleted: &deleted}
839 err = c.UpdateClient(id, change)
841 if err == ErrClientNotFound {
842 errors = append(errors, requestError{Slug: requestErrNotFound})
843 encode(w, r, http.StatusNotFound, response{Errors: errors})
846 log.Println("Error deleting client:", err)
847 errors = append(errors, requestError{Slug: requestErrActOfGod})
848 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
851 encode(w, r, http.StatusOK, response{Errors: errors})
852 go cleanUpAfterClientDeletion(id, c)
856 func AddEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) {
857 type addEndpointReq struct {
858 Endpoints []string `json:"endpoints"`
860 errors := []requestError{}
862 if vars["id"] == "" {
863 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
864 encode(w, r, http.StatusBadRequest, response{Errors: errors})
867 id, err := uuid.Parse(vars["id"])
869 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
870 encode(w, r, http.StatusBadRequest, response{Errors: errors})
873 username, password, ok := r.BasicAuth()
875 errors = append(errors, requestError{Slug: requestErrAccessDenied})
876 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
879 profile, err := authenticate(username, password, c)
881 if isAuthError(err) {
882 errors = append(errors, requestError{Slug: requestErrAccessDenied})
883 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
885 errors = append(errors, requestError{Slug: requestErrActOfGod})
886 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
890 client, err := c.GetClient(id)
892 if err == ErrClientNotFound {
893 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
894 encode(w, r, http.StatusBadRequest, response{Errors: errors})
897 errors = append(errors, requestError{Slug: requestErrActOfGod})
898 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
901 if !client.OwnerID.Equal(profile.ID) {
902 errors = append(errors, requestError{Slug: requestErrAccessDenied})
903 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
906 var req addEndpointReq
907 decoder := json.NewDecoder(r.Body)
908 err = decoder.Decode(&req)
910 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
913 if len(req.Endpoints) < 1 {
914 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/endpoints"})
915 encode(w, r, http.StatusBadRequest, response{Errors: errors})
918 endpoints := []Endpoint{}
919 for pos, u := range req.Endpoints {
920 if parsed, err := url.Parse(u); err != nil {
921 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
923 } else if !parsed.IsAbs() {
924 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints" + strconv.Itoa(pos)})
933 endpoints = append(endpoints, e)
936 encode(w, r, http.StatusBadRequest, response{Errors: errors})
939 err = c.AddEndpoints(endpoints)
941 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
946 Endpoints: endpoints,
948 encode(w, r, http.StatusCreated, resp)
951 func ListEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) {
952 errors := []requestError{}
954 clientID, err := uuid.Parse(vars["id"])
956 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "client_id"})
957 encode(w, r, http.StatusBadRequest, response{Errors: errors})
960 num := defaultEndpointResponseSize
962 numStr := r.URL.Query().Get("num")
963 offsetStr := r.URL.Query().Get("offset")
965 num, err = strconv.Atoi(numStr)
967 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"})
969 if num > maxEndpointResponseSize {
970 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"})
974 offset, err = strconv.Atoi(offsetStr)
976 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"})
980 encode(w, r, http.StatusBadRequest, response{Errors: errors})
983 endpoints, err := c.ListEndpoints(clientID, num, offset)
985 errors = append(errors, requestError{Slug: requestErrActOfGod})
986 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
990 Endpoints: endpoints,
993 encode(w, r, http.StatusOK, resp)
996 func RemoveEndpointHandler(w http.ResponseWriter, r *http.Request, c Context) {
997 errors := []requestError{}
999 if vars["client_id"] == "" {
1000 errors = append(errors, requestError{Slug: requestErrMissing, Param: "client_id"})
1001 encode(w, r, http.StatusBadRequest, response{Errors: errors})
1004 clientID, err := uuid.Parse(vars["client_id"])
1006 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "client_id"})
1007 encode(w, r, http.StatusBadRequest, response{Errors: errors})
1010 if vars["id"] == "" {
1011 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
1012 encode(w, r, http.StatusBadRequest, response{Errors: errors})
1015 id, err := uuid.Parse(vars["id"])
1017 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
1018 encode(w, r, http.StatusBadRequest, response{Errors: errors})
1021 username, password, ok := r.BasicAuth()
1023 errors = append(errors, requestError{Slug: requestErrAccessDenied})
1024 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
1027 profile, err := authenticate(username, password, c)
1029 if isAuthError(err) {
1030 errors = append(errors, requestError{Slug: requestErrAccessDenied})
1031 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
1033 errors = append(errors, requestError{Slug: requestErrActOfGod})
1034 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
1038 client, err := c.GetClient(clientID)
1040 if err == ErrClientNotFound {
1041 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "client_id"})
1042 encode(w, r, http.StatusBadRequest, response{Errors: errors})
1045 errors = append(errors, requestError{Slug: requestErrActOfGod})
1046 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
1049 if !client.OwnerID.Equal(profile.ID) {
1050 errors = append(errors, requestError{Slug: requestErrAccessDenied})
1051 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
1054 endpoint, err := c.GetEndpoint(clientID, id)
1056 if err == ErrEndpointNotFound {
1057 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
1058 encode(w, r, http.StatusBadRequest, response{Errors: errors})
1061 errors = append(errors, requestError{Slug: requestErrActOfGod})
1062 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
1065 err = c.RemoveEndpoint(clientID, id)
1067 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
1072 Endpoints: []Endpoint{endpoint},
1074 encode(w, r, http.StatusCreated, resp)
1077 func clientCredentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes Scopes, profileID uuid.ID, valid bool) {
1078 scopes = stringsToScopes(strings.Split(r.PostFormValue("scope"), " "))
1083 func clientCredentialsAuditString(r *http.Request) string {
1084 return "client_credentials"