auth

Paddy 2014-12-14 Parent:3a1fe5ee17f5 Child:c03b5eb3179e

106:d442523df640 Go to Latest

auth/profile.go

Init Config, add profile handlers, and add grant template. Call config.Init() before attempting to use it. Register our profile handlers with the router. Define a simple get_grant template for the authorization grant endpoint.

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"
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 }
88 // ApplyChange applies the properties of the passed ProfileChange
89 // to the Profile it is called on.
90 func (p *Profile) ApplyChange(change ProfileChange) {
91 if change.Name != nil {
92 p.Name = *change.Name
93 }
94 if change.Passphrase != nil {
95 p.Passphrase = *change.Passphrase
96 }
97 if change.Iterations != nil {
98 p.Iterations = *change.Iterations
99 }
100 if change.Salt != nil {
101 p.Salt = *change.Salt
102 }
103 if change.PassphraseScheme != nil {
104 p.PassphraseScheme = *change.PassphraseScheme
105 }
106 if change.Compromised != nil {
107 p.Compromised = *change.Compromised
108 }
109 if change.LockedUntil != nil {
110 p.LockedUntil = *change.LockedUntil
111 }
112 if change.PassphraseReset != nil {
113 p.PassphraseReset = *change.PassphraseReset
114 }
115 if change.PassphraseResetCreated != nil {
116 p.PassphraseResetCreated = *change.PassphraseResetCreated
117 }
118 if change.LastSeen != nil {
119 p.LastSeen = *change.LastSeen
120 }
121 }
123 // ApplyBulkChange applies the properties of the passed BulkProfileChange
124 // to the Profile it is called on.
125 func (p *Profile) ApplyBulkChange(change BulkProfileChange) {
126 if change.Compromised != nil {
127 p.Compromised = *change.Compromised
128 }
129 }
131 // ProfileChange represents a single atomic change to a Profile's mutable data.
132 type ProfileChange struct {
133 Name *string
134 Passphrase *string
135 Iterations *int
136 Salt *string
137 PassphraseScheme *int
138 Compromised *bool
139 LockedUntil *time.Time
140 PassphraseReset *string
141 PassphraseResetCreated *time.Time
142 LastSeen *time.Time
143 }
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 {
150 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 {
151 return ErrEmptyChange
152 }
153 if c.PassphraseScheme != nil && c.Passphrase == nil {
154 return ErrMissingPassphrase
155 }
156 if c.PassphraseReset != nil && c.PassphraseResetCreated == nil {
157 return ErrMissingPassphraseResetCreated
158 }
159 if c.PassphraseReset == nil && c.PassphraseResetCreated != nil {
160 return ErrMissingPassphraseReset
161 }
162 if c.Salt != nil && c.Passphrase == nil {
163 return ErrMissingPassphrase
164 }
165 if c.Iterations != nil && c.Passphrase == nil {
166 return ErrMissingPassphrase
167 }
168 if c.Passphrase != nil && len(*c.Passphrase) < MinPassphraseLength {
169 return ErrPassphraseTooShort
170 }
171 if c.Passphrase != nil && len(*c.Passphrase) > MaxPassphraseLength {
172 return ErrPassphraseTooLong
173 }
174 return nil
175 }
177 // BulkProfileChange represents a single atomic change to many Profiles' mutable data.
178 // It is a subset of a ProfileChange, as it doesn't make sense to mutate some of the
179 // ProfileChange values across many Profiles all at once.
180 type BulkProfileChange struct {
181 Compromised *bool
182 }
184 // Validate checks the BulkProfileChange it is called on
185 // and asserts its internal validity, or lack thereof.
186 // A descriptive error will be returned in the case of an
187 // invalid change.
188 func (b BulkProfileChange) Validate() error {
189 if b.Compromised == nil {
190 return ErrEmptyChange
191 }
192 return nil
193 }
195 // Login represents a single human-friendly identifier for
196 // a given Profile that can be used to log into that Profile.
197 // Each Profile may only have one Login for each Type.
198 type Login struct {
199 Type string `json:"type,omitempty"`
200 Value string `json:"value,omitempty"`
201 ProfileID uuid.ID `json:"profile_id,omitempty"`
202 Created time.Time `json:"created,omitempty"`
203 LastUsed time.Time `json:"last_used,omitempty"`
204 }
206 type newProfileRequest struct {
207 Username string `json:"username"`
208 Email string `json:"email"`
209 Passphrase string `json:"passphrase"`
210 Name string `json:"name"`
211 }
213 func validateNewProfileRequest(req *newProfileRequest) []requestError {
214 errors := []requestError{}
215 req.Name = strings.TrimSpace(req.Name)
216 req.Email = strings.TrimSpace(req.Email)
217 req.Username = slug.SlugAscii(strings.TrimSpace(req.Username))
218 if len(req.Passphrase) < MinPassphraseLength {
219 errors = append(errors, requestError{
220 Slug: requestErrInsufficient,
221 Field: "/passphrase",
222 })
223 }
224 if len(req.Passphrase) > MaxPassphraseLength {
225 errors = append(errors, requestError{
226 Slug: requestErrOverflow,
227 Field: "/passphrase",
228 })
229 }
230 if len(req.Name) > MaxNameLength {
231 errors = append(errors, requestError{
232 Slug: requestErrOverflow,
233 Field: "/name",
234 })
235 }
236 if len(req.Username) > MaxUsernameLength {
237 errors = append(errors, requestError{
238 Slug: requestErrOverflow,
239 Field: "/username",
240 })
241 }
242 if req.Email == "" {
243 errors = append(errors, requestError{
244 Slug: requestErrMissing,
245 Field: "/email",
246 })
247 }
248 if len(req.Email) > MaxEmailLength {
249 errors = append(errors, requestError{
250 Slug: requestErrOverflow,
251 Field: "/email",
252 })
253 }
254 re := regexp.MustCompile(".+@.+\\..+")
255 if !re.Match([]byte(req.Email)) {
256 errors = append(errors, requestError{
257 Slug: requestErrInvalidFormat,
258 Field: "/email",
259 })
260 }
261 return errors
262 }
264 type profileStore interface {
265 getProfileByID(id uuid.ID) (Profile, error)
266 getProfileByLogin(value string) (Profile, error)
267 saveProfile(profile Profile) error
268 updateProfile(id uuid.ID, change ProfileChange) error
269 updateProfiles(ids []uuid.ID, change BulkProfileChange) error
270 deleteProfile(id uuid.ID) error
272 addLogin(login Login) error
273 removeLogin(value string, profile uuid.ID) error
274 recordLoginUse(value string, when time.Time) error
275 listLogins(profile uuid.ID, num, offset int) ([]Login, error)
276 }
278 func (m *memstore) getProfileByID(id uuid.ID) (Profile, error) {
279 m.profileLock.RLock()
280 defer m.profileLock.RUnlock()
281 p, ok := m.profiles[id.String()]
282 if !ok {
283 return Profile{}, ErrProfileNotFound
284 }
285 return p, nil
286 }
288 func (m *memstore) getProfileByLogin(value string) (Profile, error) {
289 m.loginLock.RLock()
290 defer m.loginLock.RUnlock()
291 login, ok := m.logins[value]
292 if !ok {
293 return Profile{}, ErrLoginNotFound
294 }
295 m.profileLock.RLock()
296 defer m.profileLock.RUnlock()
297 profile, ok := m.profiles[login.ProfileID.String()]
298 if !ok {
299 return Profile{}, ErrProfileNotFound
300 }
301 return profile, nil
302 }
304 func (m *memstore) saveProfile(profile Profile) error {
305 m.profileLock.Lock()
306 defer m.profileLock.Unlock()
307 _, ok := m.profiles[profile.ID.String()]
308 if ok {
309 return ErrProfileAlreadyExists
310 }
311 m.profiles[profile.ID.String()] = profile
312 return nil
313 }
315 func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error {
316 m.profileLock.Lock()
317 defer m.profileLock.Unlock()
318 p, ok := m.profiles[id.String()]
319 if !ok {
320 return ErrProfileNotFound
321 }
322 p.ApplyChange(change)
323 m.profiles[id.String()] = p
324 return nil
325 }
327 func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error {
328 m.profileLock.Lock()
329 defer m.profileLock.Unlock()
330 for id, profile := range m.profiles {
331 for _, i := range ids {
332 if id == i.String() {
333 profile.ApplyBulkChange(change)
334 m.profiles[id] = profile
335 break
336 }
337 }
338 }
339 return nil
340 }
342 func (m *memstore) deleteProfile(id uuid.ID) error {
343 m.profileLock.Lock()
344 defer m.profileLock.Unlock()
345 _, ok := m.profiles[id.String()]
346 if !ok {
347 return ErrProfileNotFound
348 }
349 delete(m.profiles, id.String())
350 return nil
351 }
353 func (m *memstore) addLogin(login Login) error {
354 m.loginLock.Lock()
355 defer m.loginLock.Unlock()
356 _, ok := m.logins[login.Value]
357 if ok {
358 return ErrLoginAlreadyExists
359 }
360 m.logins[login.Value] = login
361 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Value)
362 return nil
363 }
365 func (m *memstore) removeLogin(value string, profile uuid.ID) error {
366 m.loginLock.Lock()
367 defer m.loginLock.Unlock()
368 l, ok := m.logins[value]
369 if !ok {
370 return ErrLoginNotFound
371 }
372 if !l.ProfileID.Equal(profile) {
373 return ErrLoginNotFound
374 }
375 delete(m.logins, value)
376 pos := -1
377 for p, id := range m.profileLoginLookup[profile.String()] {
378 if id == value {
379 pos = p
380 break
381 }
382 }
383 if pos >= 0 {
384 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...)
385 }
386 return nil
387 }
389 func (m *memstore) recordLoginUse(value string, when time.Time) error {
390 m.loginLock.Lock()
391 defer m.loginLock.Unlock()
392 l, ok := m.logins[value]
393 if !ok {
394 return ErrLoginNotFound
395 }
396 l.LastUsed = when
397 m.logins[value] = l
398 return nil
399 }
401 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) {
402 m.loginLock.RLock()
403 defer m.loginLock.RUnlock()
404 ids, ok := m.profileLoginLookup[profile.String()]
405 if !ok {
406 return []Login{}, nil
407 }
408 if len(ids) > num+offset {
409 ids = ids[offset : num+offset]
410 } else if len(ids) > offset {
411 ids = ids[offset:]
412 } else {
413 return []Login{}, nil
414 }
415 logins := []Login{}
416 for _, id := range ids {
417 login, ok := m.logins[id]
418 if !ok {
419 continue
420 }
421 logins = append(logins, login)
422 }
423 return logins, nil
424 }
426 // RegisterProfileHandlers adds handlers to the passed router to handle the profile endpoints, like registration and user retrieval.
427 func RegisterProfileHandlers(r *mux.Router, context Context) {
428 r.Handle("/profiles", wrap(context, CreateProfileHandler)).Methods("POST")
429 }
431 // CreateProfileHandler is an HTTP handler for registering new profiles.
432 func CreateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
433 scheme, ok := passphraseSchemes[CurPassphraseScheme]
434 if !ok {
435 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
436 return
437 }
438 var req newProfileRequest
439 errors := []requestError{}
440 decoder := json.NewDecoder(r.Body)
441 err := decoder.Decode(&req)
442 if err != nil {
443 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
444 return
445 }
446 errors = append(errors, validateNewProfileRequest(&req)...)
447 if len(errors) > 0 {
448 encode(w, r, http.StatusBadRequest, response{Errors: errors})
449 return
450 }
451 passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations)
452 if err != nil {
453 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
454 return
455 }
456 profile := Profile{
457 ID: uuid.NewID(),
458 Name: req.Name,
459 Passphrase: string(passphrase),
460 Iterations: context.config.iterations,
461 Salt: string(salt),
462 PassphraseScheme: CurPassphraseScheme,
463 Created: time.Now(),
464 LastSeen: time.Now(),
465 }
466 err = context.SaveProfile(profile)
467 if err != nil {
468 if err == ErrProfileAlreadyExists {
469 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/id"}}})
470 return
471 }
472 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
473 return
474 }
475 logins := []Login{}
476 login := Login{
477 Type: "email",
478 Value: req.Email,
479 Created: profile.Created,
480 LastUsed: profile.Created,
481 ProfileID: profile.ID,
482 }
483 err = context.AddLogin(login)
484 if err != nil {
485 if err == ErrLoginAlreadyExists {
486 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/email"}}})
487 return
488 }
489 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
490 return
491 }
492 logins = append(logins, login)
493 if req.Username != "" {
494 login.Type = "username"
495 login.Value = req.Username
496 err = context.AddLogin(login)
497 if err != nil {
498 if err == ErrLoginAlreadyExists {
499 encode(w, r, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrConflict, Field: "/username"}}})
500 return
501 }
502 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
503 return
504 }
505 logins = append(logins, login)
506 }
507 resp := response{
508 Logins: logins,
509 Profiles: []Profile{profile},
510 }
511 encode(w, r, http.StatusCreated, resp)
512 // TODO(paddy): should we kick off the email validation flow?
513 }