auth

Paddy 2015-01-19 Parent:c03b5eb3179e Child:23c1a07c8a61

126:34de07217709 Go to Latest

auth/profile.go

Test around client types and secrets. Implement a test that the CreateClient handler will correctly create a confidential client and issue a secret for it. Also, just generally test that clients that are confidential are issued secrets and clients that are public are not.

History
1 package auth
3 import (
4 "encoding/json"
5 "errors"
6 "net/http"
7 "regexp"
8 "strings"
9 "time"
11 "code.secondbit.org/uuid.hg"
12 "github.com/extemporalgenome/slug"
13 "github.com/gorilla/mux"
14 )
16 const (
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.
24 MaxNameLength = 64
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.
28 MaxEmailLength = 64
29 )
31 var (
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")
67 )
69 // Profile represents a single user of the service,
70 // including their authentication information, but not
71 // including their username or email.
72 type Profile struct {
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"`
85 }
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 {
91 p.Name = *change.Name
92 }
93 if change.Passphrase != nil {
94 p.Passphrase = *change.Passphrase
95 }
96 if change.Iterations != nil {
97 p.Iterations = *change.Iterations
98 }
99 if change.Salt != nil {
100 p.Salt = *change.Salt
101 }
102 if change.PassphraseScheme != nil {
103 p.PassphraseScheme = *change.PassphraseScheme
104 }
105 if change.Compromised != nil {
106 p.Compromised = *change.Compromised
107 }
108 if change.LockedUntil != nil {
109 p.LockedUntil = *change.LockedUntil
110 }
111 if change.PassphraseReset != nil {
112 p.PassphraseReset = *change.PassphraseReset
113 }
114 if change.PassphraseResetCreated != nil {
115 p.PassphraseResetCreated = *change.PassphraseResetCreated
116 }
117 if change.LastSeen != nil {
118 p.LastSeen = *change.LastSeen
119 }
120 }
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
127 }
128 }
130 // ProfileChange represents a single atomic change to a Profile's mutable data.
131 type ProfileChange struct {
132 Name *string
133 Passphrase *string
134 Iterations *int
135 Salt *string
136 PassphraseScheme *int
137 Compromised *bool
138 LockedUntil *time.Time
139 PassphraseReset *string
140 PassphraseResetCreated *time.Time
141 LastSeen *time.Time
142 }
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
151 }
152 if c.PassphraseScheme != nil && c.Passphrase == nil {
153 return ErrMissingPassphrase
154 }
155 if c.PassphraseReset != nil && c.PassphraseResetCreated == nil {
156 return ErrMissingPassphraseResetCreated
157 }
158 if c.PassphraseReset == nil && c.PassphraseResetCreated != nil {
159 return ErrMissingPassphraseReset
160 }
161 if c.Salt != nil && c.Passphrase == nil {
162 return ErrMissingPassphrase
163 }
164 if c.Iterations != nil && c.Passphrase == nil {
165 return ErrMissingPassphrase
166 }
167 if c.Passphrase != nil && len(*c.Passphrase) < MinPassphraseLength {
168 return ErrPassphraseTooShort
169 }
170 if c.Passphrase != nil && len(*c.Passphrase) > MaxPassphraseLength {
171 return ErrPassphraseTooLong
172 }
173 return nil
174 }
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 {
180 Compromised *bool
181 }
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
186 // invalid change.
187 func (b BulkProfileChange) Validate() error {
188 if b.Compromised == nil {
189 return ErrEmptyChange
190 }
191 return nil
192 }
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.
197 type Login struct {
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"`
203 }
205 type newProfileRequest struct {
206 Username string `json:"username"`
207 Email string `json:"email"`
208 Passphrase string `json:"passphrase"`
209 Name string `json:"name"`
210 }
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",
221 })
222 }
223 if len(req.Passphrase) > MaxPassphraseLength {
224 errors = append(errors, requestError{
225 Slug: requestErrOverflow,
226 Field: "/passphrase",
227 })
228 }
229 if len(req.Name) > MaxNameLength {
230 errors = append(errors, requestError{
231 Slug: requestErrOverflow,
232 Field: "/name",
233 })
234 }
235 if len(req.Username) > MaxUsernameLength {
236 errors = append(errors, requestError{
237 Slug: requestErrOverflow,
238 Field: "/username",
239 })
240 }
241 if req.Email == "" {
242 errors = append(errors, requestError{
243 Slug: requestErrMissing,
244 Field: "/email",
245 })
246 }
247 if len(req.Email) > MaxEmailLength {
248 errors = append(errors, requestError{
249 Slug: requestErrOverflow,
250 Field: "/email",
251 })
252 }
253 re := regexp.MustCompile(".+@.+\\..+")
254 if !re.Match([]byte(req.Email)) {
255 errors = append(errors, requestError{
256 Slug: requestErrInvalidFormat,
257 Field: "/email",
258 })
259 }
260 return errors
261 }
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)
275 }
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()]
281 if !ok {
282 return Profile{}, ErrProfileNotFound
283 }
284 return p, nil
285 }
287 func (m *memstore) getProfileByLogin(value string) (Profile, error) {
288 m.loginLock.RLock()
289 defer m.loginLock.RUnlock()
290 login, ok := m.logins[value]
291 if !ok {
292 return Profile{}, ErrLoginNotFound
293 }
294 m.profileLock.RLock()
295 defer m.profileLock.RUnlock()
296 profile, ok := m.profiles[login.ProfileID.String()]
297 if !ok {
298 return Profile{}, ErrProfileNotFound
299 }
300 return profile, nil
301 }
303 func (m *memstore) saveProfile(profile Profile) error {
304 m.profileLock.Lock()
305 defer m.profileLock.Unlock()
306 _, ok := m.profiles[profile.ID.String()]
307 if ok {
308 return ErrProfileAlreadyExists
309 }
310 m.profiles[profile.ID.String()] = profile
311 return nil
312 }
314 func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error {
315 m.profileLock.Lock()
316 defer m.profileLock.Unlock()
317 p, ok := m.profiles[id.String()]
318 if !ok {
319 return ErrProfileNotFound
320 }
321 p.ApplyChange(change)
322 m.profiles[id.String()] = p
323 return nil
324 }
326 func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error {
327 m.profileLock.Lock()
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
334 break
335 }
336 }
337 }
338 return nil
339 }
341 func (m *memstore) deleteProfile(id uuid.ID) error {
342 m.profileLock.Lock()
343 defer m.profileLock.Unlock()
344 _, ok := m.profiles[id.String()]
345 if !ok {
346 return ErrProfileNotFound
347 }
348 delete(m.profiles, id.String())
349 return nil
350 }
352 func (m *memstore) addLogin(login Login) error {
353 m.loginLock.Lock()
354 defer m.loginLock.Unlock()
355 _, ok := m.logins[login.Value]
356 if ok {
357 return ErrLoginAlreadyExists
358 }
359 m.logins[login.Value] = login
360 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Value)
361 return nil
362 }
364 func (m *memstore) removeLogin(value string, profile uuid.ID) error {
365 m.loginLock.Lock()
366 defer m.loginLock.Unlock()
367 l, ok := m.logins[value]
368 if !ok {
369 return ErrLoginNotFound
370 }
371 if !l.ProfileID.Equal(profile) {
372 return ErrLoginNotFound
373 }
374 delete(m.logins, value)
375 pos := -1
376 for p, id := range m.profileLoginLookup[profile.String()] {
377 if id == value {
378 pos = p
379 break
380 }
381 }
382 if pos >= 0 {
383 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...)
384 }
385 return nil
386 }
388 func (m *memstore) recordLoginUse(value string, when time.Time) error {
389 m.loginLock.Lock()
390 defer m.loginLock.Unlock()
391 l, ok := m.logins[value]
392 if !ok {
393 return ErrLoginNotFound
394 }
395 l.LastUsed = when
396 m.logins[value] = l
397 return nil
398 }
400 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) {
401 m.loginLock.RLock()
402 defer m.loginLock.RUnlock()
403 ids, ok := m.profileLoginLookup[profile.String()]
404 if !ok {
405 return []Login{}, nil
406 }
407 if len(ids) > num+offset {
408 ids = ids[offset : num+offset]
409 } else if len(ids) > offset {
410 ids = ids[offset:]
411 } else {
412 return []Login{}, nil
413 }
414 logins := []Login{}
415 for _, id := range ids {
416 login, ok := m.logins[id]
417 if !ok {
418 continue
419 }
420 logins = append(logins, login)
421 }
422 return logins, nil
423 }
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")
428 }
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]
433 if !ok {
434 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
435 return
436 }
437 var req newProfileRequest
438 errors := []requestError{}
439 decoder := json.NewDecoder(r.Body)
440 err := decoder.Decode(&req)
441 if err != nil {
442 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
443 return
444 }
445 errors = append(errors, validateNewProfileRequest(&req)...)
446 if len(errors) > 0 {
447 encode(w, r, http.StatusBadRequest, response{Errors: errors})
448 return
449 }
450 passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations)
451 if err != nil {
452 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
453 return
454 }
455 profile := Profile{
456 ID: uuid.NewID(),
457 Name: req.Name,
458 Passphrase: string(passphrase),
459 Iterations: context.config.iterations,
460 Salt: string(salt),
461 PassphraseScheme: CurPassphraseScheme,
462 Created: time.Now(),
463 LastSeen: time.Now(),
464 }
465 err = context.SaveProfile(profile)
466 if err != nil {
467 if err == ErrProfileAlreadyExists {
468 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/id"}}})
469 return
470 }
471 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
472 return
473 }
474 logins := []Login{}
475 login := Login{
476 Type: "email",
477 Value: req.Email,
478 Created: profile.Created,
479 LastUsed: profile.Created,
480 ProfileID: profile.ID,
481 }
482 err = context.AddLogin(login)
483 if err != nil {
484 if err == ErrLoginAlreadyExists {
485 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/email"}}})
486 return
487 }
488 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
489 return
490 }
491 logins = append(logins, login)
492 if req.Username != "" {
493 login.Type = "username"
494 login.Value = req.Username
495 err = context.AddLogin(login)
496 if err != nil {
497 if err == ErrLoginAlreadyExists {
498 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/username"}}})
499 return
500 }
501 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
502 return
503 }
504 logins = append(logins, login)
505 }
506 resp := response{
507 Logins: logins,
508 Profiles: []Profile{profile},
509 }
510 encode(w, r, http.StatusCreated, resp)
511 // TODO(paddy): should we kick off the email validation flow?
512 }