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.
12 "code.secondbit.org/uuid.hg"
13 "github.com/gorilla/mux"
17 // MinPassphraseLength is the minimum length, in bytes, of a passphrase, exclusive.
18 MinPassphraseLength = 6
19 // MaxPassphraseLength is the maximum length, in bytes, of a passphrase, exclusive.
20 MaxPassphraseLength = 64
21 // CurPassphraseScheme is the current passphrase scheme. Incrememnt it when we use a different passphrase scheme
22 CurPassphraseScheme = 1
23 // MaxNameLength is the maximum length, in bytes, of a name, exclusive.
25 // MaxEmailLength is the maximum length, in bytes, of an email address, exclusive.
30 // ErrNoProfileStore is returned when a Context tries to act on a profileStore without setting one first.
31 ErrNoProfileStore = errors.New("no profileStore was specified for the Context")
32 // ErrProfileAlreadyExists is returned when a Profile is added to a profileStore, but another Profile with
33 // the same ID already exists in the profileStore.
34 ErrProfileAlreadyExists = errors.New("profile already exists in profileStore")
35 // ErrProfileNotFound is returned when a Profile is requested but not found in the profileStore.
36 ErrProfileNotFound = errors.New("profile not found in profileStore")
37 // ErrLoginAlreadyExists is returned when a Login is added to a profileStore, but another Login with the same
38 // Type and Value already exists in the profileStore.
39 ErrLoginAlreadyExists = errors.New("login already exists in profileStore")
40 // ErrLoginNotFound is returned when a Login is requested but not found in the profileStore.
41 ErrLoginNotFound = errors.New("login not found in profileStore")
43 // ErrMissingPassphrase is returned when a ProfileChange is validated but does not contain a
44 // Passphrase, and requires one.
45 ErrMissingPassphrase = errors.New("missing passphrase")
46 // ErrMissingPassphraseReset is returned when a ProfileChange is validated but does not contain
47 // a PassphraseReset, and requires one.
48 ErrMissingPassphraseReset = errors.New("missing passphrase reset")
49 // ErrMissingPassphraseResetCreated is returned when a ProfileChange is validated but does not
50 // contain a PassphraseResetCreated, and requires one.
51 ErrMissingPassphraseResetCreated = errors.New("missing passphrase reset created timestamp")
52 // ErrPassphraseTooShort is returned when a ProfileChange is validated and contains a Passphrase,
53 // but the Passphrase is shorter than MinPassphraseLength.
54 ErrPassphraseTooShort = errors.New("passphrase too short")
55 // ErrPassphraseTooLong is returned when a ProfileChange is validated and contains a Passphrase,
56 // but the Passphrase is longer than MaxPassphraseLength.
57 ErrPassphraseTooLong = errors.New("passphrase too long")
59 // ErrProfileCompromised is returned when a user tries to log in with a profile that is suspected
60 // of being compromised.
61 ErrProfileCompromised = errors.New("profile compromised")
62 // ErrProfileLocked is returned when a user tries to log in with a profile that is locked for a certain
63 // duration, to prevent brute force attacks.
64 ErrProfileLocked = errors.New("profile locked")
67 // Profile represents a single user of the service,
68 // including their authentication information.
70 ID uuid.ID `json:"id,omitempty"`
71 Name string `json:"name,omitempty"`
72 Passphrase string `json:"-"`
73 Iterations int `json:"-"`
74 Salt string `json:"-"`
75 PassphraseScheme int `json:"-"`
76 Compromised bool `json:"-"`
77 LockedUntil time.Time `json:"-"`
78 PassphraseReset string `json:"-"`
79 PassphraseResetCreated time.Time `json:"-"`
80 Created time.Time `json:"created,omitempty"`
81 LastSeen time.Time `json:"last_seen,omitempty"`
84 // ApplyChange applies the properties of the passed ProfileChange
85 // to the Profile it is called on.
86 func (p *Profile) ApplyChange(change ProfileChange) {
87 if change.Name != nil {
90 if change.Passphrase != nil {
91 p.Passphrase = *change.Passphrase
93 if change.Iterations != nil {
94 p.Iterations = *change.Iterations
96 if change.Salt != nil {
99 if change.PassphraseScheme != nil {
100 p.PassphraseScheme = *change.PassphraseScheme
102 if change.Compromised != nil {
103 p.Compromised = *change.Compromised
105 if change.LockedUntil != nil {
106 p.LockedUntil = *change.LockedUntil
108 if change.PassphraseReset != nil {
109 p.PassphraseReset = *change.PassphraseReset
111 if change.PassphraseResetCreated != nil {
112 p.PassphraseResetCreated = *change.PassphraseResetCreated
114 if change.LastSeen != nil {
115 p.LastSeen = *change.LastSeen
119 // ApplyBulkChange applies the properties of the passed BulkProfileChange
120 // to the Profile it is called on.
121 func (p *Profile) ApplyBulkChange(change BulkProfileChange) {
122 if change.Compromised != nil {
123 p.Compromised = *change.Compromised
127 // ProfileChange represents a single atomic change to a Profile's mutable data.
128 type ProfileChange struct {
133 PassphraseScheme *int
135 LockedUntil *time.Time
136 PassphraseReset *string
137 PassphraseResetCreated *time.Time
141 func (c ProfileChange) Empty() bool {
142 return (c.Name == nil && c.Passphrase == nil && c.Iterations == nil && c.Salt == nil && c.PassphraseScheme == nil && c.Compromised == nil && c.LockedUntil == nil && c.PassphraseReset == nil && c.PassphraseResetCreated == nil && c.LastSeen == nil)
145 // Validate checks the ProfileChange it is called on
146 // and asserts its internal validity, or lack thereof.
147 // A descriptive error will be returned in the case of
148 // an invalid change.
149 func (c ProfileChange) Validate() error {
151 return ErrEmptyChange
153 if c.PassphraseScheme != nil && c.Passphrase == nil {
154 return ErrMissingPassphrase
156 if c.PassphraseReset != nil && c.PassphraseResetCreated == nil {
157 return ErrMissingPassphraseResetCreated
159 if c.PassphraseReset == nil && c.PassphraseResetCreated != nil {
160 return ErrMissingPassphraseReset
162 if c.Salt != nil && c.Passphrase == nil {
163 return ErrMissingPassphrase
165 if c.Iterations != nil && c.Passphrase == nil {
166 return ErrMissingPassphrase
171 // BulkProfileChange represents a single atomic change to many Profiles' mutable data.
172 // It is a subset of a ProfileChange, as it doesn't make sense to mutate some of the
173 // ProfileChange values across many Profiles all at once.
174 type BulkProfileChange struct {
178 func (b BulkProfileChange) Empty() bool {
179 return b.Compromised == nil
182 // Validate checks the BulkProfileChange it is called on
183 // and asserts its internal validity, or lack thereof.
184 // A descriptive error will be returned in the case of an
186 func (b BulkProfileChange) Validate() error {
188 return ErrEmptyChange
193 // Login represents a single human-friendly identifier for
194 // a given Profile that can be used to log into that Profile.
195 // Each Profile may only have one Login for each Type.
197 Type string `json:"type,omitempty"`
198 Value string `json:"value,omitempty"`
199 ProfileID uuid.ID `json:"profile_id,omitempty"`
200 Created time.Time `json:"created,omitempty"`
201 LastUsed time.Time `json:"last_used,omitempty"`
204 type newProfileRequest struct {
205 Email string `json:"email"`
206 Passphrase string `json:"passphrase"`
207 Name string `json:"name"`
210 func validateNewProfileRequest(req *newProfileRequest) []requestError {
211 errors := []requestError{}
212 req.Name = strings.TrimSpace(req.Name)
213 req.Email = strings.TrimSpace(req.Email)
214 if len(req.Passphrase) < MinPassphraseLength {
215 errors = append(errors, requestError{
216 Slug: requestErrInsufficient,
217 Field: "/passphrase",
220 if len(req.Passphrase) > MaxPassphraseLength {
221 errors = append(errors, requestError{
222 Slug: requestErrOverflow,
223 Field: "/passphrase",
226 if len(req.Name) > MaxNameLength {
227 errors = append(errors, requestError{
228 Slug: requestErrOverflow,
233 errors = append(errors, requestError{
234 Slug: requestErrMissing,
238 if len(req.Email) > MaxEmailLength {
239 errors = append(errors, requestError{
240 Slug: requestErrOverflow,
244 re := regexp.MustCompile(".+@.+\\..+")
245 if !re.Match([]byte(req.Email)) {
246 errors = append(errors, requestError{
247 Slug: requestErrInvalidFormat,
254 type profileStore interface {
255 getProfileByID(id uuid.ID) (Profile, error)
256 getProfileByLogin(value string) (Profile, error)
257 saveProfile(profile Profile) error
258 updateProfile(id uuid.ID, change ProfileChange) error
259 updateProfiles(ids []uuid.ID, change BulkProfileChange) error
260 deleteProfile(id uuid.ID) error
262 addLogin(login Login) error
263 removeLogin(value string, profile uuid.ID) error
264 removeLoginsByProfile(profile uuid.ID) error
265 recordLoginUse(value string, when time.Time) error
266 listLogins(profile uuid.ID, num, offset int) ([]Login, error)
269 func (m *memstore) getProfileByID(id uuid.ID) (Profile, error) {
270 m.profileLock.RLock()
271 defer m.profileLock.RUnlock()
272 p, ok := m.profiles[id.String()]
274 return Profile{}, ErrProfileNotFound
279 func (m *memstore) getProfileByLogin(value string) (Profile, error) {
281 defer m.loginLock.RUnlock()
282 login, ok := m.logins[value]
284 return Profile{}, ErrLoginNotFound
286 m.profileLock.RLock()
287 defer m.profileLock.RUnlock()
288 profile, ok := m.profiles[login.ProfileID.String()]
290 return Profile{}, ErrProfileNotFound
295 func (m *memstore) saveProfile(profile Profile) error {
297 defer m.profileLock.Unlock()
298 _, ok := m.profiles[profile.ID.String()]
300 return ErrProfileAlreadyExists
302 m.profiles[profile.ID.String()] = profile
306 func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error {
308 defer m.profileLock.Unlock()
309 p, ok := m.profiles[id.String()]
311 return ErrProfileNotFound
313 p.ApplyChange(change)
314 m.profiles[id.String()] = p
318 func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error {
320 defer m.profileLock.Unlock()
321 for id, profile := range m.profiles {
322 for _, i := range ids {
323 if id == i.String() {
324 profile.ApplyBulkChange(change)
325 m.profiles[id] = profile
333 func (m *memstore) deleteProfile(id uuid.ID) error {
335 defer m.profileLock.Unlock()
336 if _, ok := m.profiles[id.String()]; !ok {
337 return ErrProfileNotFound
339 delete(m.profiles, id.String())
343 func (m *memstore) addLogin(login Login) error {
345 defer m.loginLock.Unlock()
346 _, ok := m.logins[login.Value]
348 return ErrLoginAlreadyExists
350 m.logins[login.Value] = login
351 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Value)
355 func (m *memstore) removeLogin(value string, profile uuid.ID) error {
357 defer m.loginLock.Unlock()
358 l, ok := m.logins[value]
360 return ErrLoginNotFound
362 if !l.ProfileID.Equal(profile) {
363 return ErrLoginNotFound
365 delete(m.logins, value)
367 for p, id := range m.profileLoginLookup[profile.String()] {
374 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...)
379 func (m *memstore) removeLoginsByProfile(profile uuid.ID) error {
381 defer m.loginLock.Unlock()
382 logins, ok := m.profileLoginLookup[profile.String()]
384 return ErrProfileNotFound
386 delete(m.profileLoginLookup, profile.String())
387 for _, login := range logins {
388 delete(m.logins, login)
393 func (m *memstore) recordLoginUse(value string, when time.Time) error {
395 defer m.loginLock.Unlock()
396 l, ok := m.logins[value]
398 return ErrLoginNotFound
405 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) {
407 defer m.loginLock.RUnlock()
408 ids, ok := m.profileLoginLookup[profile.String()]
410 return []Login{}, nil
412 if len(ids) > num+offset {
413 ids = ids[offset : num+offset]
414 } else if len(ids) > offset {
417 return []Login{}, nil
420 for _, id := range ids {
421 login, ok := m.logins[id]
425 logins = append(logins, login)
430 func cleanUpAfterProfileDeletion(profile uuid.ID, context Context) {
431 err := context.RemoveLoginsByProfile(profile)
433 log.Printf("Error removing logins from profile %s: %+v\n", profile, err)
435 err = context.TerminateSessionsByProfile(profile)
437 log.Printf("Error terminating sessions associated with profile %s: %+v\n", profile, err)
439 err = context.RevokeTokensByProfileID(profile)
441 log.Printf("Error revoking tokens associated with profile %s: %+v\n", profile, err)
443 err = context.DeleteAuthorizationCodesByProfileID(profile)
445 log.Printf("Error deleting authorization codes associated with profile %s: %+v\n", profile, err)
447 clients, err := context.ListClientsByOwner(profile, -1, 0)
449 log.Printf("Error listing clients by profile %s: %+v\n", profile, err)
451 err = context.DeleteClientsByOwner(profile)
453 log.Printf("Error deleting clients by profile %s: %+v\n", profile, err)
455 for _, client := range clients {
456 cleanUpAfterClientDeletion(client.ID, context)
460 // RegisterProfileHandlers adds handlers to the passed router to handle the profile endpoints, like registration and user retrieval.
461 func RegisterProfileHandlers(r *mux.Router, context Context) {
462 r.Handle("/profiles", wrap(context, CreateProfileHandler)).Methods("POST", "OPTIONS")
463 r.Handle("/profiles/{id}", wrap(context, GetProfileHandler)).Methods("GET", "OPTIONS")
464 r.Handle("/profiles/{id}", wrap(context, UpdateProfileHandler)).Methods("PATCH", "OPTIONS")
465 r.Handle("/profiles/{id}", wrap(context, DeleteProfileHandler)).Methods("DELETE", "OPTIONS")
466 // BUG(paddy): We need to implement a handler that will add a login to a profile.
467 // BUG(paddy): We need to implement a handler that will remove a login from a profile. What happens to sessions created with that login?
468 // BUG(paddy): We need to implement a handler that will list the logins attached to a profile.
471 // GetProfileHandler is an HTTP handler for retrieving a profile.
472 func GetProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
473 errors := []requestError{}
474 authz := r.Header.Get("Authorization")
475 if !strings.HasPrefix(authz, "Bearer ") {
476 errors = append(errors, requestError{Slug: requestErrAccessDenied})
477 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
480 authz = strings.TrimPrefix(authz, "Bearer ")
482 if vars["id"] == "" {
483 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
484 encode(w, r, http.StatusBadRequest, response{Errors: errors})
487 id, err := uuid.Parse(vars["id"])
489 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"})
490 encode(w, r, http.StatusBadRequest, response{Errors: errors})
493 token, err := context.GetToken(authz, false)
494 if err != nil || token.Revoked {
495 if err == ErrTokenNotFound || token.Revoked {
496 errors = append(errors, requestError{Slug: requestErrAccessDenied})
497 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
500 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
504 if !id.Equal(token.ProfileID) {
505 errors = append(errors, requestError{Slug: requestErrAccessDenied})
506 encode(w, r, http.StatusForbidden, response{Errors: errors})
509 profile, err := context.GetProfileByID(id)
511 if err == ErrProfileNotFound {
512 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
513 encode(w, r, http.StatusNotFound, response{Errors: errors})
516 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
519 encode(w, r, http.StatusOK, response{Profiles: []Profile{profile}})
523 // CreateProfileHandler is an HTTP handler for registering new profiles.
524 func CreateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
525 scheme, ok := passphraseSchemes[CurPassphraseScheme]
527 log.Printf("Error selecting passphrase scheme #%d\n", CurPassphraseScheme)
528 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
531 var req newProfileRequest
532 errors := []requestError{}
533 decoder := json.NewDecoder(r.Body)
534 err := decoder.Decode(&req)
536 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
539 errors = append(errors, validateNewProfileRequest(&req)...)
541 encode(w, r, http.StatusBadRequest, response{Errors: errors})
544 passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations)
546 log.Printf("Error creating encoded passphrase: %#+v\n", err)
547 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
553 Passphrase: string(passphrase),
554 Iterations: context.config.iterations,
556 PassphraseScheme: CurPassphraseScheme,
558 LastSeen: time.Now(),
560 err = context.SaveProfile(profile)
562 if err == ErrProfileAlreadyExists {
563 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/id"}}})
566 log.Printf("Error saving profile: %#+v\n", err)
567 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
574 Created: profile.Created,
575 LastUsed: profile.Created,
576 ProfileID: profile.ID,
578 err = context.AddLogin(login)
580 if err == ErrLoginAlreadyExists {
581 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/email"}}})
584 log.Printf("Error adding login: %#+v\n", err)
585 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
588 logins = append(logins, login)
591 Profiles: []Profile{profile},
593 encode(w, r, http.StatusCreated, resp)
594 // TODO(paddy): should we kick off the email validation flow?
597 func UpdateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
598 errors := []requestError{}
600 if vars["id"] == "" {
601 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
602 encode(w, r, http.StatusBadRequest, response{Errors: errors})
605 id, err := uuid.Parse(vars["id"])
607 errors = append(errors, requestError{Slug: requestErrAccessDenied})
608 encode(w, r, http.StatusBadRequest, response{Errors: errors})
611 username, password, ok := r.BasicAuth()
613 errors = append(errors, requestError{Slug: requestErrAccessDenied})
614 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
617 profile, err := authenticate(username, password, context)
619 if isAuthError(err) {
620 errors = append(errors, requestError{Slug: requestErrAccessDenied})
621 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
623 errors = append(errors, requestError{Slug: requestErrActOfGod})
624 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
628 if !profile.ID.Equal(id) {
629 errors = append(errors, requestError{Slug: requestErrAccessDenied})
630 encode(w, r, http.StatusForbidden, response{Errors: errors})
633 var req ProfileChange
634 decoder := json.NewDecoder(r.Body)
635 err = decoder.Decode(&req)
637 log.Printf("Error decoding request: %#+v\n", err)
638 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
643 req.PassphraseScheme = nil
644 req.Compromised = nil // BUG(paddy): Need a way for admins to mark accounts as compromised
645 req.LockedUntil = nil
647 if req.Passphrase != nil {
648 if len(*req.Passphrase) < MinPassphraseLength {
649 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/passphrase"})
650 encode(w, r, http.StatusBadRequest, response{Errors: errors})
653 if len(*req.Passphrase) > MaxPassphraseLength {
654 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/passphrase"})
655 encode(w, r, http.StatusBadRequest, response{Errors: errors})
658 iterations := context.config.iterations
659 scheme, ok := passphraseSchemes[CurPassphraseScheme]
661 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
664 curScheme := CurPassphraseScheme
665 req.PassphraseScheme = &curScheme
666 passphrase, salt, err := scheme.create(*req.Passphrase, iterations)
668 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
671 req.Passphrase = &passphrase
673 req.Iterations = &iterations
675 if req.PassphraseReset != nil {
677 req.PassphraseResetCreated = &now
685 resp.Profiles = []Profile{profile}
686 status = http.StatusOK
688 errors = append(errors, requestError{Slug: requestErrActOfGod})
690 status = http.StatusInternalServerError
692 encode(w, r, status, resp)
695 err = context.UpdateProfile(id, req)
697 if err == ErrProfileNotFound {
698 errors = append(errors, requestError{Slug: requestErrNotFound})
699 encode(w, r, http.StatusNotFound, response{Errors: errors})
702 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
705 profile.ApplyChange(req)
706 encode(w, r, http.StatusOK, response{Profiles: []Profile{profile}})
710 func DeleteProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
711 errors := []requestError{}
713 if vars["id"] == "" {
714 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
715 encode(w, r, http.StatusBadRequest, response{Errors: errors})
718 id, err := uuid.Parse(vars["id"])
720 errors = append(errors, requestError{Slug: requestErrAccessDenied})
721 encode(w, r, http.StatusBadRequest, response{Errors: errors})
724 username, password, ok := r.BasicAuth()
726 errors = append(errors, requestError{Slug: requestErrAccessDenied})
727 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
730 profile, err := authenticate(username, password, context)
732 if isAuthError(err) {
733 errors = append(errors, requestError{Slug: requestErrAccessDenied})
734 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
736 errors = append(errors, requestError{Slug: requestErrActOfGod})
737 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
741 if !profile.ID.Equal(id) {
742 errors = append(errors, requestError{Slug: requestErrAccessDenied})
743 encode(w, r, http.StatusForbidden, response{Errors: errors})
746 err = context.DeleteProfile(id)
748 if err == ErrProfileNotFound {
749 errors = append(errors, requestError{Slug: requestErrNotFound})
750 encode(w, r, http.StatusNotFound, response{Errors: errors})
753 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
756 encode(w, r, http.StatusOK, response{Profiles: []Profile{profile}})
757 go cleanUpAfterProfileDeletion(profile.ID, context)