auth

Paddy 2015-04-11 Parent:8267e1c8bcd1 Child:3223a8e679db

157:202e991accc2 Go to Latest

auth/profile.go

Wire up the postgres database for authd. Have authd use the AUTH_PG_DB environment variable to detect support for the postgres *Stores, and if postgres is supported, use it. If postgres isn't supported, fall back on the in-memory store. Also create-if-not-exists the test scopes, instead of panicking when the scope already exists.

History
1 package auth
3 import (
4 "encoding/json"
5 "errors"
6 "log"
7 "net/http"
8 "regexp"
9 "strings"
10 "time"
12 "code.secondbit.org/uuid.hg"
13 "github.com/extemporalgenome/slug"
14 "github.com/gorilla/mux"
15 )
17 const (
18 // MinPassphraseLength is the minimum length, in bytes, of a passphrase, exclusive.
19 MinPassphraseLength = 6
20 // MaxPassphraseLength is the maximum length, in bytes, of a passphrase, exclusive.
21 MaxPassphraseLength = 64
22 // CurPassphraseScheme is the current passphrase scheme. Incrememnt it when we use a different passphrase scheme
23 CurPassphraseScheme = 1
24 // MaxNameLength is the maximum length, in bytes, of a name, exclusive.
25 MaxNameLength = 64
26 // MaxUsernameLength is the maximum length, in bytes, of a username, exclusive.
27 MaxUsernameLength = 16
28 // MaxEmailLength is the maximum length, in bytes, of an email address, exclusive.
29 MaxEmailLength = 64
30 )
32 var (
33 // ErrNoProfileStore is returned when a Context tries to act on a profileStore without setting one first.
34 ErrNoProfileStore = errors.New("no profileStore was specified for the Context")
35 // ErrProfileAlreadyExists is returned when a Profile is added to a profileStore, but another Profile with
36 // the same ID already exists in the profileStore.
37 ErrProfileAlreadyExists = errors.New("profile already exists in profileStore")
38 // ErrProfileNotFound is returned when a Profile is requested but not found in the profileStore.
39 ErrProfileNotFound = errors.New("profile not found in profileStore")
40 // ErrLoginAlreadyExists is returned when a Login is added to a profileStore, but another Login with the same
41 // Type and Value already exists in the profileStore.
42 ErrLoginAlreadyExists = errors.New("login already exists in profileStore")
43 // ErrLoginNotFound is returned when a Login is requested but not found in the profileStore.
44 ErrLoginNotFound = errors.New("login not found in profileStore")
46 // ErrMissingPassphrase is returned when a ProfileChange is validated but does not contain a
47 // Passphrase, and requires one.
48 ErrMissingPassphrase = errors.New("missing passphrase")
49 // ErrMissingPassphraseReset is returned when a ProfileChange is validated but does not contain
50 // a PassphraseReset, and requires one.
51 ErrMissingPassphraseReset = errors.New("missing passphrase reset")
52 // ErrMissingPassphraseResetCreated is returned when a ProfileChange is validated but does not
53 // contain a PassphraseResetCreated, and requires one.
54 ErrMissingPassphraseResetCreated = errors.New("missing passphrase reset created timestamp")
55 // ErrPassphraseTooShort is returned when a ProfileChange is validated and contains a Passphrase,
56 // but the Passphrase is shorter than MinPassphraseLength.
57 ErrPassphraseTooShort = errors.New("passphrase too short")
58 // ErrPassphraseTooLong is returned when a ProfileChange is validated and contains a Passphrase,
59 // but the Passphrase is longer than MaxPassphraseLength.
60 ErrPassphraseTooLong = errors.New("passphrase too long")
62 // ErrProfileCompromised is returned when a user tries to log in with a profile that is suspected
63 // of being compromised.
64 ErrProfileCompromised = errors.New("profile compromised")
65 // ErrProfileLocked is returned when a user tries to log in with a profile that is locked for a certain
66 // duration, to prevent brute force attacks.
67 ErrProfileLocked = errors.New("profile locked")
68 )
70 // Profile represents a single user of the service,
71 // including their authentication information, but not
72 // including their username or email.
73 type Profile struct {
74 ID uuid.ID `json:"id,omitempty"`
75 Name string `json:"name,omitempty"`
76 Passphrase string `json:"-"`
77 Iterations int `json:"-"`
78 Salt string `json:"-"`
79 PassphraseScheme int `json:"-"`
80 Compromised bool `json:"-"`
81 LockedUntil time.Time `json:"-"`
82 PassphraseReset string `json:"-"`
83 PassphraseResetCreated time.Time `json:"-"`
84 Created time.Time `json:"created,omitempty"`
85 LastSeen time.Time `json:"last_seen,omitempty"`
86 Deleted bool `json:"deleted,omitempty"`
87 }
89 // ApplyChange applies the properties of the passed ProfileChange
90 // to the Profile it is called on.
91 func (p *Profile) ApplyChange(change ProfileChange) {
92 if change.Name != nil {
93 p.Name = *change.Name
94 }
95 if change.Passphrase != nil {
96 p.Passphrase = *change.Passphrase
97 }
98 if change.Iterations != nil {
99 p.Iterations = *change.Iterations
100 }
101 if change.Salt != nil {
102 p.Salt = *change.Salt
103 }
104 if change.PassphraseScheme != nil {
105 p.PassphraseScheme = *change.PassphraseScheme
106 }
107 if change.Compromised != nil {
108 p.Compromised = *change.Compromised
109 }
110 if change.LockedUntil != nil {
111 p.LockedUntil = *change.LockedUntil
112 }
113 if change.PassphraseReset != nil {
114 p.PassphraseReset = *change.PassphraseReset
115 }
116 if change.PassphraseResetCreated != nil {
117 p.PassphraseResetCreated = *change.PassphraseResetCreated
118 }
119 if change.LastSeen != nil {
120 p.LastSeen = *change.LastSeen
121 }
122 if change.Deleted != nil {
123 p.Deleted = *change.Deleted
124 }
125 }
127 // ApplyBulkChange applies the properties of the passed BulkProfileChange
128 // to the Profile it is called on.
129 func (p *Profile) ApplyBulkChange(change BulkProfileChange) {
130 if change.Compromised != nil {
131 p.Compromised = *change.Compromised
132 }
133 }
135 // ProfileChange represents a single atomic change to a Profile's mutable data.
136 type ProfileChange struct {
137 Name *string
138 Passphrase *string
139 Iterations *int
140 Salt *string
141 PassphraseScheme *int
142 Compromised *bool
143 LockedUntil *time.Time
144 PassphraseReset *string
145 PassphraseResetCreated *time.Time
146 LastSeen *time.Time
147 Deleted *bool
148 }
150 func (c ProfileChange) Empty() bool {
151 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 && c.Deleted == nil)
152 }
154 // Validate checks the ProfileChange it is called on
155 // and asserts its internal validity, or lack thereof.
156 // A descriptive error will be returned in the case of
157 // an invalid change.
158 func (c ProfileChange) Validate() error {
159 if c.Empty() {
160 return ErrEmptyChange
161 }
162 if c.PassphraseScheme != nil && c.Passphrase == nil {
163 return ErrMissingPassphrase
164 }
165 if c.PassphraseReset != nil && c.PassphraseResetCreated == nil {
166 return ErrMissingPassphraseResetCreated
167 }
168 if c.PassphraseReset == nil && c.PassphraseResetCreated != nil {
169 return ErrMissingPassphraseReset
170 }
171 if c.Salt != nil && c.Passphrase == nil {
172 return ErrMissingPassphrase
173 }
174 if c.Iterations != nil && c.Passphrase == nil {
175 return ErrMissingPassphrase
176 }
177 return nil
178 }
180 // BulkProfileChange represents a single atomic change to many Profiles' mutable data.
181 // It is a subset of a ProfileChange, as it doesn't make sense to mutate some of the
182 // ProfileChange values across many Profiles all at once.
183 type BulkProfileChange struct {
184 Compromised *bool
185 }
187 func (b BulkProfileChange) Empty() bool {
188 return b.Compromised == nil
189 }
191 // Validate checks the BulkProfileChange it is called on
192 // and asserts its internal validity, or lack thereof.
193 // A descriptive error will be returned in the case of an
194 // invalid change.
195 func (b BulkProfileChange) Validate() error {
196 if b.Empty() {
197 return ErrEmptyChange
198 }
199 return nil
200 }
202 // Login represents a single human-friendly identifier for
203 // a given Profile that can be used to log into that Profile.
204 // Each Profile may only have one Login for each Type.
205 type Login struct {
206 Type string `json:"type,omitempty"`
207 Value string `json:"value,omitempty"`
208 ProfileID uuid.ID `json:"profile_id,omitempty"`
209 Created time.Time `json:"created,omitempty"`
210 LastUsed time.Time `json:"last_used,omitempty"`
211 }
213 type newProfileRequest struct {
214 Username string `json:"username"`
215 Email string `json:"email"`
216 Passphrase string `json:"passphrase"`
217 Name string `json:"name"`
218 }
220 func validateNewProfileRequest(req *newProfileRequest) []requestError {
221 errors := []requestError{}
222 req.Name = strings.TrimSpace(req.Name)
223 req.Email = strings.TrimSpace(req.Email)
224 req.Username = slug.SlugAscii(strings.TrimSpace(req.Username))
225 if len(req.Passphrase) < MinPassphraseLength {
226 errors = append(errors, requestError{
227 Slug: requestErrInsufficient,
228 Field: "/passphrase",
229 })
230 }
231 if len(req.Passphrase) > MaxPassphraseLength {
232 errors = append(errors, requestError{
233 Slug: requestErrOverflow,
234 Field: "/passphrase",
235 })
236 }
237 if len(req.Name) > MaxNameLength {
238 errors = append(errors, requestError{
239 Slug: requestErrOverflow,
240 Field: "/name",
241 })
242 }
243 if len(req.Username) > MaxUsernameLength {
244 errors = append(errors, requestError{
245 Slug: requestErrOverflow,
246 Field: "/username",
247 })
248 }
249 if req.Email == "" {
250 errors = append(errors, requestError{
251 Slug: requestErrMissing,
252 Field: "/email",
253 })
254 }
255 if len(req.Email) > MaxEmailLength {
256 errors = append(errors, requestError{
257 Slug: requestErrOverflow,
258 Field: "/email",
259 })
260 }
261 re := regexp.MustCompile(".+@.+\\..+")
262 if !re.Match([]byte(req.Email)) {
263 errors = append(errors, requestError{
264 Slug: requestErrInvalidFormat,
265 Field: "/email",
266 })
267 }
268 return errors
269 }
271 type profileStore interface {
272 getProfileByID(id uuid.ID) (Profile, error)
273 getProfileByLogin(value string) (Profile, error)
274 saveProfile(profile Profile) error
275 updateProfile(id uuid.ID, change ProfileChange) error
276 updateProfiles(ids []uuid.ID, change BulkProfileChange) error
278 addLogin(login Login) error
279 removeLogin(value string, profile uuid.ID) error
280 recordLoginUse(value string, when time.Time) error
281 listLogins(profile uuid.ID, num, offset int) ([]Login, error)
282 }
284 func (m *memstore) getProfileByID(id uuid.ID) (Profile, error) {
285 m.profileLock.RLock()
286 defer m.profileLock.RUnlock()
287 p, ok := m.profiles[id.String()]
288 if !ok {
289 return Profile{}, ErrProfileNotFound
290 }
291 if p.Deleted {
292 return Profile{}, ErrProfileNotFound
293 }
294 return p, nil
295 }
297 func (m *memstore) getProfileByLogin(value string) (Profile, error) {
298 m.loginLock.RLock()
299 defer m.loginLock.RUnlock()
300 login, ok := m.logins[value]
301 if !ok {
302 return Profile{}, ErrLoginNotFound
303 }
304 m.profileLock.RLock()
305 defer m.profileLock.RUnlock()
306 profile, ok := m.profiles[login.ProfileID.String()]
307 if !ok {
308 return Profile{}, ErrProfileNotFound
309 }
310 if profile.Deleted {
311 return Profile{}, ErrProfileNotFound
312 }
313 return profile, nil
314 }
316 func (m *memstore) saveProfile(profile Profile) error {
317 m.profileLock.Lock()
318 defer m.profileLock.Unlock()
319 _, ok := m.profiles[profile.ID.String()]
320 if ok {
321 return ErrProfileAlreadyExists
322 }
323 m.profiles[profile.ID.String()] = profile
324 return nil
325 }
327 func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error {
328 m.profileLock.Lock()
329 defer m.profileLock.Unlock()
330 p, ok := m.profiles[id.String()]
331 if !ok {
332 return ErrProfileNotFound
333 }
334 p.ApplyChange(change)
335 m.profiles[id.String()] = p
336 return nil
337 }
339 func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error {
340 m.profileLock.Lock()
341 defer m.profileLock.Unlock()
342 for id, profile := range m.profiles {
343 for _, i := range ids {
344 if id == i.String() {
345 profile.ApplyBulkChange(change)
346 m.profiles[id] = profile
347 break
348 }
349 }
350 }
351 return nil
352 }
354 func (m *memstore) addLogin(login Login) error {
355 m.loginLock.Lock()
356 defer m.loginLock.Unlock()
357 _, ok := m.logins[login.Value]
358 if ok {
359 return ErrLoginAlreadyExists
360 }
361 m.logins[login.Value] = login
362 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Value)
363 return nil
364 }
366 func (m *memstore) removeLogin(value string, profile uuid.ID) error {
367 m.loginLock.Lock()
368 defer m.loginLock.Unlock()
369 l, ok := m.logins[value]
370 if !ok {
371 return ErrLoginNotFound
372 }
373 if !l.ProfileID.Equal(profile) {
374 return ErrLoginNotFound
375 }
376 delete(m.logins, value)
377 pos := -1
378 for p, id := range m.profileLoginLookup[profile.String()] {
379 if id == value {
380 pos = p
381 break
382 }
383 }
384 if pos >= 0 {
385 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...)
386 }
387 return nil
388 }
390 func (m *memstore) recordLoginUse(value string, when time.Time) error {
391 m.loginLock.Lock()
392 defer m.loginLock.Unlock()
393 l, ok := m.logins[value]
394 if !ok {
395 return ErrLoginNotFound
396 }
397 l.LastUsed = when
398 m.logins[value] = l
399 return nil
400 }
402 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) {
403 m.loginLock.RLock()
404 defer m.loginLock.RUnlock()
405 ids, ok := m.profileLoginLookup[profile.String()]
406 if !ok {
407 return []Login{}, nil
408 }
409 if len(ids) > num+offset {
410 ids = ids[offset : num+offset]
411 } else if len(ids) > offset {
412 ids = ids[offset:]
413 } else {
414 return []Login{}, nil
415 }
416 logins := []Login{}
417 for _, id := range ids {
418 login, ok := m.logins[id]
419 if !ok {
420 continue
421 }
422 logins = append(logins, login)
423 }
424 return logins, nil
425 }
427 // RegisterProfileHandlers adds handlers to the passed router to handle the profile endpoints, like registration and user retrieval.
428 func RegisterProfileHandlers(r *mux.Router, context Context) {
429 r.Handle("/profiles", wrap(context, CreateProfileHandler)).Methods("POST")
430 // BUG(paddy): We need to implement a handler that will return information about a profile or set of profiles.
431 r.Handle("/profiles/{id}", wrap(context, UpdateProfileHandler)).Methods("PATCH")
432 // BUG(paddy): We need to implement a handler that will delete a profile. What happens to clients/tokens/grants/sessions when a profile is deleted?
433 // BUG(paddy): We need to implement a handler that will add a login to a profile.
434 // BUG(paddy): We need to implement a handler that will remove a login from a profile. What happens to sessions created with that login?
435 // BUG(paddy): We need to implement a handler that will list the logins attached to a profile.
436 }
438 // CreateProfileHandler is an HTTP handler for registering new profiles.
439 func CreateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
440 scheme, ok := passphraseSchemes[CurPassphraseScheme]
441 if !ok {
442 log.Printf("Error selecting passphrase scheme #%d\n", CurPassphraseScheme)
443 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
444 return
445 }
446 var req newProfileRequest
447 errors := []requestError{}
448 decoder := json.NewDecoder(r.Body)
449 err := decoder.Decode(&req)
450 if err != nil {
451 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
452 return
453 }
454 errors = append(errors, validateNewProfileRequest(&req)...)
455 if len(errors) > 0 {
456 encode(w, r, http.StatusBadRequest, response{Errors: errors})
457 return
458 }
459 passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations)
460 if err != nil {
461 log.Printf("Error creating encoded passphrase: %#+v\n", err)
462 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
463 return
464 }
465 profile := Profile{
466 ID: uuid.NewID(),
467 Name: req.Name,
468 Passphrase: string(passphrase),
469 Iterations: context.config.iterations,
470 Salt: string(salt),
471 PassphraseScheme: CurPassphraseScheme,
472 Created: time.Now(),
473 LastSeen: time.Now(),
474 }
475 err = context.SaveProfile(profile)
476 if err != nil {
477 if err == ErrProfileAlreadyExists {
478 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/id"}}})
479 return
480 }
481 log.Printf("Error saving profile: %#+v\n", err)
482 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
483 return
484 }
485 logins := []Login{}
486 login := Login{
487 Type: "email",
488 Value: req.Email,
489 Created: profile.Created,
490 LastUsed: profile.Created,
491 ProfileID: profile.ID,
492 }
493 err = context.AddLogin(login)
494 if err != nil {
495 if err == ErrLoginAlreadyExists {
496 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/email"}}})
497 return
498 }
499 log.Printf("Error adding login: %#+v\n", err)
500 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
501 return
502 }
503 logins = append(logins, login)
504 if req.Username != "" {
505 login.Type = "username"
506 login.Value = req.Username
507 err = context.AddLogin(login)
508 if err != nil {
509 if err == ErrLoginAlreadyExists {
510 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/username"}}})
511 return
512 }
513 log.Printf("Error adding login: %#+v\n", err)
514 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
515 return
516 }
517 logins = append(logins, login)
518 }
519 resp := response{
520 Logins: logins,
521 Profiles: []Profile{profile},
522 }
523 encode(w, r, http.StatusCreated, resp)
524 // TODO(paddy): should we kick off the email validation flow?
525 }
527 func UpdateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
528 errors := []requestError{}
529 vars := mux.Vars(r)
530 if vars["id"] == "" {
531 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
532 encode(w, r, http.StatusBadRequest, response{Errors: errors})
533 return
534 }
535 id, err := uuid.Parse(vars["id"])
536 if err != nil {
537 errors = append(errors, requestError{Slug: requestErrAccessDenied})
538 encode(w, r, http.StatusBadRequest, response{Errors: errors})
539 return
540 }
541 username, password, ok := r.BasicAuth()
542 if !ok {
543 errors = append(errors, requestError{Slug: requestErrAccessDenied})
544 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
545 return
546 }
547 profile, err := authenticate(username, password, context)
548 if err != nil {
549 if isAuthError(err) {
550 errors = append(errors, requestError{Slug: requestErrAccessDenied})
551 encode(w, r, http.StatusUnauthorized, response{Errors: errors})
552 } else {
553 errors = append(errors, requestError{Slug: requestErrActOfGod})
554 encode(w, r, http.StatusInternalServerError, response{Errors: errors})
555 }
556 return
557 }
558 if !profile.ID.Equal(id) {
559 errors = append(errors, requestError{Slug: requestErrAccessDenied})
560 encode(w, r, http.StatusForbidden, response{Errors: errors})
561 return
562 }
563 var req ProfileChange
564 decoder := json.NewDecoder(r.Body)
565 err = decoder.Decode(&req)
566 if err != nil {
567 log.Printf("Error decoding request: %#+v\n", err)
568 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
569 return
570 }
571 req.Iterations = nil
572 req.Salt = nil
573 req.PassphraseScheme = nil
574 req.Compromised = nil // BUG(paddy): Need a way for admins to mark accounts as compromised
575 req.LockedUntil = nil
576 req.LastSeen = nil
577 if req.Passphrase != nil {
578 if len(*req.Passphrase) < MinPassphraseLength {
579 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/passphrase"})
580 encode(w, r, http.StatusBadRequest, response{Errors: errors})
581 return
582 }
583 if len(*req.Passphrase) > MaxPassphraseLength {
584 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/passphrase"})
585 encode(w, r, http.StatusBadRequest, response{Errors: errors})
586 return
587 }
588 iterations := context.config.iterations
589 scheme, ok := passphraseSchemes[CurPassphraseScheme]
590 if !ok {
591 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
592 return
593 }
594 curScheme := CurPassphraseScheme
595 req.PassphraseScheme = &curScheme
596 passphrase, salt, err := scheme.create(*req.Passphrase, iterations)
597 if err != nil {
598 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
599 return
600 }
601 req.Passphrase = &passphrase
602 req.Salt = &salt
603 req.Iterations = &iterations
604 }
605 if req.PassphraseReset != nil {
606 now := time.Now()
607 req.PassphraseResetCreated = &now
608 }
609 err = req.Validate()
610 if err != nil {
611 var status int
612 var resp response
613 switch err {
614 case ErrEmptyChange:
615 resp.Profiles = []Profile{profile}
616 status = http.StatusOK
617 default:
618 errors = append(errors, requestError{Slug: requestErrActOfGod})
619 resp.Errors = errors
620 status = http.StatusInternalServerError
621 }
622 encode(w, r, status, resp)
623 return
624 }
625 err = context.UpdateProfile(id, req)
626 if err != nil {
627 if err == ErrProfileNotFound {
628 errors = append(errors, requestError{Slug: requestErrNotFound})
629 encode(w, r, http.StatusNotFound, response{Errors: errors})
630 return
631 }
632 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
633 return
634 }
635 profile.ApplyChange(req)
636 encode(w, r, http.StatusOK, response{Profiles: []Profile{profile}})
637 return
638 }