Refactor verifyClient, implement refresh tokens.
Refactor verifyClient into verifyClient and getClientAuth. We moved verifyClient
out of each of the GrantType's validation functions and into the access token
endpoint, where it will be called before the GrantType's validation function.
Yay, less code repetition. And seeing as we always want to verify the client,
that seems like a good way to prevent things like 118a69954621 from happening.
This did, however, force us to add an AllowsPublic property to the GrantType, so
the token endpoint knows whether or not a public Client is valid for any given
GrantType.
We also implemented the refresh token grant type, which required adding ClientID
and RefreshRevoked as properties on the Token type. We need ClientID because we
need to constrain refresh tokens to the client that issued them. We also should
probably keep track of which tokens belong to which clients, just as a general
rule of thumb. RefreshRevoked had to be created, next to Revoked, because the
AccessToken could be revoked and the RefreshToken still valid, or vice versa.
Notably, when you issue a new refresh token, the old one is revoked, but the
access token is still valid. It remains to be seen whether this is a good way to
track things or not. The number of duplicated properties lead me to believe our
type is not a great representation of the underlying concepts.
11 "code.secondbit.org/uuid.hg"
12 "github.com/extemporalgenome/slug"
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 // MaxUsernameLength is the maximum length, in bytes, of a username, exclusive.
26 MaxUsernameLength = 16
27 // MaxEmailLength is the maximum length, in bytes, of an email address, exclusive.
32 // ErrNoProfileStore is returned when a Context tries to act on a profileStore without setting one first.
33 ErrNoProfileStore = errors.New("no profileStore was specified for the Context")
34 // ErrProfileAlreadyExists is returned when a Profile is added to a profileStore, but another Profile with
35 // the same ID already exists in the profileStore.
36 ErrProfileAlreadyExists = errors.New("profile already exists in profileStore")
37 // ErrProfileNotFound is returned when a Profile is requested but not found in the profileStore.
38 ErrProfileNotFound = errors.New("profile not found in profileStore")
39 // ErrLoginAlreadyExists is returned when a Login is added to a profileStore, but another Login with the same
40 // Type and Value already exists in the profileStore.
41 ErrLoginAlreadyExists = errors.New("login already exists in profileStore")
42 // ErrLoginNotFound is returned when a Login is requested but not found in the profileStore.
43 ErrLoginNotFound = errors.New("login not found in profileStore")
45 // ErrMissingPassphrase is returned when a ProfileChange is validated but does not contain a
46 // Passphrase, and requires one.
47 ErrMissingPassphrase = errors.New("missing passphrase")
48 // ErrMissingPassphraseReset is returned when a ProfileChange is validated but does not contain
49 // a PassphraseReset, and requires one.
50 ErrMissingPassphraseReset = errors.New("missing passphrase reset")
51 // ErrMissingPassphraseResetCreated is returned when a ProfileChange is validated but does not
52 // contain a PassphraseResetCreated, and requires one.
53 ErrMissingPassphraseResetCreated = errors.New("missing passphrase reset created timestamp")
54 // ErrPassphraseTooShort is returned when a ProfileChange is validated and contains a Passphrase,
55 // but the Passphrase is shorter than MinPassphraseLength.
56 ErrPassphraseTooShort = errors.New("passphrase too short")
57 // ErrPassphraseTooLong is returned when a ProfileChange is validated and contains a Passphrase,
58 // but the Passphrase is longer than MaxPassphraseLength.
59 ErrPassphraseTooLong = errors.New("passphrase too long")
61 // ErrProfileCompromised is returned when a user tries to log in with a profile that is suspected
62 // of being compromised.
63 ErrProfileCompromised = errors.New("profile compromised")
64 // ErrProfileLocked is returned when a user tries to log in with a profile that is locked for a certain
65 // duration, to prevent brute force attacks.
66 ErrProfileLocked = errors.New("profile locked")
69 // Profile represents a single user of the service,
70 // including their authentication information, but not
71 // including their username or email.
73 ID uuid.ID `json:"id,omitempty"`
74 Name string `json:"name,omitempty"`
75 Passphrase string `json:"-"`
76 Iterations int `json:"-"`
77 Salt string `json:"-"`
78 PassphraseScheme int `json:"-"`
79 Compromised bool `json:"-"`
80 LockedUntil time.Time `json:"-"`
81 PassphraseReset string `json:"-"`
82 PassphraseResetCreated time.Time `json:"-"`
83 Created time.Time `json:"created,omitempty"`
84 LastSeen time.Time `json:"last_seen,omitempty"`
87 // ApplyChange applies the properties of the passed ProfileChange
88 // to the Profile it is called on.
89 func (p *Profile) ApplyChange(change ProfileChange) {
90 if change.Name != nil {
93 if change.Passphrase != nil {
94 p.Passphrase = *change.Passphrase
96 if change.Iterations != nil {
97 p.Iterations = *change.Iterations
99 if change.Salt != nil {
100 p.Salt = *change.Salt
102 if change.PassphraseScheme != nil {
103 p.PassphraseScheme = *change.PassphraseScheme
105 if change.Compromised != nil {
106 p.Compromised = *change.Compromised
108 if change.LockedUntil != nil {
109 p.LockedUntil = *change.LockedUntil
111 if change.PassphraseReset != nil {
112 p.PassphraseReset = *change.PassphraseReset
114 if change.PassphraseResetCreated != nil {
115 p.PassphraseResetCreated = *change.PassphraseResetCreated
117 if change.LastSeen != nil {
118 p.LastSeen = *change.LastSeen
122 // ApplyBulkChange applies the properties of the passed BulkProfileChange
123 // to the Profile it is called on.
124 func (p *Profile) ApplyBulkChange(change BulkProfileChange) {
125 if change.Compromised != nil {
126 p.Compromised = *change.Compromised
130 // ProfileChange represents a single atomic change to a Profile's mutable data.
131 type ProfileChange struct {
136 PassphraseScheme *int
138 LockedUntil *time.Time
139 PassphraseReset *string
140 PassphraseResetCreated *time.Time
144 // Validate checks the ProfileChange it is called on
145 // and asserts its internal validity, or lack thereof.
146 // A descriptive error will be returned in the case of
147 // an invalid change.
148 func (c ProfileChange) Validate() error {
149 if 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 {
150 return ErrEmptyChange
152 if c.PassphraseScheme != nil && c.Passphrase == nil {
153 return ErrMissingPassphrase
155 if c.PassphraseReset != nil && c.PassphraseResetCreated == nil {
156 return ErrMissingPassphraseResetCreated
158 if c.PassphraseReset == nil && c.PassphraseResetCreated != nil {
159 return ErrMissingPassphraseReset
161 if c.Salt != nil && c.Passphrase == nil {
162 return ErrMissingPassphrase
164 if c.Iterations != nil && c.Passphrase == nil {
165 return ErrMissingPassphrase
167 if c.Passphrase != nil && len(*c.Passphrase) < MinPassphraseLength {
168 return ErrPassphraseTooShort
170 if c.Passphrase != nil && len(*c.Passphrase) > MaxPassphraseLength {
171 return ErrPassphraseTooLong
176 // BulkProfileChange represents a single atomic change to many Profiles' mutable data.
177 // It is a subset of a ProfileChange, as it doesn't make sense to mutate some of the
178 // ProfileChange values across many Profiles all at once.
179 type BulkProfileChange struct {
183 // Validate checks the BulkProfileChange it is called on
184 // and asserts its internal validity, or lack thereof.
185 // A descriptive error will be returned in the case of an
187 func (b BulkProfileChange) Validate() error {
188 if b.Compromised == nil {
189 return ErrEmptyChange
194 // Login represents a single human-friendly identifier for
195 // a given Profile that can be used to log into that Profile.
196 // Each Profile may only have one Login for each Type.
198 Type string `json:"type,omitempty"`
199 Value string `json:"value,omitempty"`
200 ProfileID uuid.ID `json:"profile_id,omitempty"`
201 Created time.Time `json:"created,omitempty"`
202 LastUsed time.Time `json:"last_used,omitempty"`
205 type newProfileRequest struct {
206 Username string `json:"username"`
207 Email string `json:"email"`
208 Passphrase string `json:"passphrase"`
209 Name string `json:"name"`
212 func validateNewProfileRequest(req *newProfileRequest) []requestError {
213 errors := []requestError{}
214 req.Name = strings.TrimSpace(req.Name)
215 req.Email = strings.TrimSpace(req.Email)
216 req.Username = slug.SlugAscii(strings.TrimSpace(req.Username))
217 if len(req.Passphrase) < MinPassphraseLength {
218 errors = append(errors, requestError{
219 Slug: requestErrInsufficient,
220 Field: "/passphrase",
223 if len(req.Passphrase) > MaxPassphraseLength {
224 errors = append(errors, requestError{
225 Slug: requestErrOverflow,
226 Field: "/passphrase",
229 if len(req.Name) > MaxNameLength {
230 errors = append(errors, requestError{
231 Slug: requestErrOverflow,
235 if len(req.Username) > MaxUsernameLength {
236 errors = append(errors, requestError{
237 Slug: requestErrOverflow,
242 errors = append(errors, requestError{
243 Slug: requestErrMissing,
247 if len(req.Email) > MaxEmailLength {
248 errors = append(errors, requestError{
249 Slug: requestErrOverflow,
253 re := regexp.MustCompile(".+@.+\\..+")
254 if !re.Match([]byte(req.Email)) {
255 errors = append(errors, requestError{
256 Slug: requestErrInvalidFormat,
263 type profileStore interface {
264 getProfileByID(id uuid.ID) (Profile, error)
265 getProfileByLogin(value string) (Profile, error)
266 saveProfile(profile Profile) error
267 updateProfile(id uuid.ID, change ProfileChange) error
268 updateProfiles(ids []uuid.ID, change BulkProfileChange) error
269 deleteProfile(id uuid.ID) error
271 addLogin(login Login) error
272 removeLogin(value string, profile uuid.ID) error
273 recordLoginUse(value string, when time.Time) error
274 listLogins(profile uuid.ID, num, offset int) ([]Login, error)
277 func (m *memstore) getProfileByID(id uuid.ID) (Profile, error) {
278 m.profileLock.RLock()
279 defer m.profileLock.RUnlock()
280 p, ok := m.profiles[id.String()]
282 return Profile{}, ErrProfileNotFound
287 func (m *memstore) getProfileByLogin(value string) (Profile, error) {
289 defer m.loginLock.RUnlock()
290 login, ok := m.logins[value]
292 return Profile{}, ErrLoginNotFound
294 m.profileLock.RLock()
295 defer m.profileLock.RUnlock()
296 profile, ok := m.profiles[login.ProfileID.String()]
298 return Profile{}, ErrProfileNotFound
303 func (m *memstore) saveProfile(profile Profile) error {
305 defer m.profileLock.Unlock()
306 _, ok := m.profiles[profile.ID.String()]
308 return ErrProfileAlreadyExists
310 m.profiles[profile.ID.String()] = profile
314 func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error {
316 defer m.profileLock.Unlock()
317 p, ok := m.profiles[id.String()]
319 return ErrProfileNotFound
321 p.ApplyChange(change)
322 m.profiles[id.String()] = p
326 func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error {
328 defer m.profileLock.Unlock()
329 for id, profile := range m.profiles {
330 for _, i := range ids {
331 if id == i.String() {
332 profile.ApplyBulkChange(change)
333 m.profiles[id] = profile
341 func (m *memstore) deleteProfile(id uuid.ID) error {
343 defer m.profileLock.Unlock()
344 _, ok := m.profiles[id.String()]
346 return ErrProfileNotFound
348 delete(m.profiles, id.String())
352 func (m *memstore) addLogin(login Login) error {
354 defer m.loginLock.Unlock()
355 _, ok := m.logins[login.Value]
357 return ErrLoginAlreadyExists
359 m.logins[login.Value] = login
360 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Value)
364 func (m *memstore) removeLogin(value string, profile uuid.ID) error {
366 defer m.loginLock.Unlock()
367 l, ok := m.logins[value]
369 return ErrLoginNotFound
371 if !l.ProfileID.Equal(profile) {
372 return ErrLoginNotFound
374 delete(m.logins, value)
376 for p, id := range m.profileLoginLookup[profile.String()] {
383 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...)
388 func (m *memstore) recordLoginUse(value string, when time.Time) error {
390 defer m.loginLock.Unlock()
391 l, ok := m.logins[value]
393 return ErrLoginNotFound
400 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) {
402 defer m.loginLock.RUnlock()
403 ids, ok := m.profileLoginLookup[profile.String()]
405 return []Login{}, nil
407 if len(ids) > num+offset {
408 ids = ids[offset : num+offset]
409 } else if len(ids) > offset {
412 return []Login{}, nil
415 for _, id := range ids {
416 login, ok := m.logins[id]
420 logins = append(logins, login)
425 // RegisterProfileHandlers adds handlers to the passed router to handle the profile endpoints, like registration and user retrieval.
426 func RegisterProfileHandlers(r *mux.Router, context Context) {
427 r.Handle("/profiles", wrap(context, CreateProfileHandler)).Methods("POST")
430 // CreateProfileHandler is an HTTP handler for registering new profiles.
431 func CreateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
432 scheme, ok := passphraseSchemes[CurPassphraseScheme]
434 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
437 var req newProfileRequest
438 errors := []requestError{}
439 decoder := json.NewDecoder(r.Body)
440 err := decoder.Decode(&req)
442 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
445 errors = append(errors, validateNewProfileRequest(&req)...)
447 encode(w, r, http.StatusBadRequest, response{Errors: errors})
450 passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations)
452 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
458 Passphrase: string(passphrase),
459 Iterations: context.config.iterations,
461 PassphraseScheme: CurPassphraseScheme,
463 LastSeen: time.Now(),
465 err = context.SaveProfile(profile)
467 if err == ErrProfileAlreadyExists {
468 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/id"}}})
471 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
478 Created: profile.Created,
479 LastUsed: profile.Created,
480 ProfileID: profile.ID,
482 err = context.AddLogin(login)
484 if err == ErrLoginAlreadyExists {
485 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/email"}}})
488 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
491 logins = append(logins, login)
492 if req.Username != "" {
493 login.Type = "username"
494 login.Value = req.Username
495 err = context.AddLogin(login)
497 if err == ErrLoginAlreadyExists {
498 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/username"}}})
501 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
504 logins = append(logins, login)
508 Profiles: []Profile{profile},
510 encode(w, r, http.StatusCreated, resp)
511 // TODO(paddy): should we kick off the email validation flow?