Write Session tests.
Add loginURI as a property to our Context, to keep track of where users should
be redirected to log in.
Implement the sessionStore in the memstore to let us test with Sessions.
Catch when the HTTP Basic Auth header doesn't include two parts, rather than
panicking. Return an ErrInvalidAuthFormat.
Clean up the error handling for checkCookie to be cleaner. Log unexpected errors
from request.Cookie.
Stop checking for cookie expiration times--those aren't sent to the server, so
we'll never get a valid session if we look for them.
Add a helper to build a login redirect URI--a URI the user can be redirected to
that has a URL-encoded URL to redirect the user back to after a successful
login.
Add a wrapper to wrap our Context into HTTP handlers.
Create a RegisterOAuth2 helper that adds the OAuth2 endpoints to a Gorilla/mux
router.
Redirect users to the login page when they have no session set or an invalid
session.
Return a server error when we can't check our cookie for whatever reason.
Log errors.
Add sessions to our OAuth2 tests so the tests stop failing--the session check
was interfering with them.
Add a test for our getBasicAuth helper to ensure that we're parsing basic auth
correctly.
Add an ErrSessionAlreadyExists error to be returned when a session has an ID
conflict.
Test that our memstore implementation of the sessionStore works as intended..
7 "code.secondbit.org/uuid"
11 // MinPassphraseLength is the minimum length, in bytes, of a passphrase, exclusive.
12 MinPassphraseLength = 6
13 // MaxPassphraseLength is the maximum length, in bytes, of a passphrase, exclusive.
14 MaxPassphraseLength = 64
15 // CurPassphraseScheme is the current passphrase scheme. Incrememnt it when we use a different passphrase scheme
16 CurPassphraseScheme = 1
20 // ErrNoProfileStore is returned when a Context tries to act on a profileStore without setting one first.
21 ErrNoProfileStore = errors.New("no profileStore was specified for the Context")
22 // ErrProfileAlreadyExists is returned when a Profile is added to a profileStore, but another Profile with
23 // the same ID already exists in the profileStore.
24 ErrProfileAlreadyExists = errors.New("profile already exists in profileStore")
25 // ErrProfileNotFound is returned when a Profile is requested but not found in the profileStore.
26 ErrProfileNotFound = errors.New("profile not found in profileStore")
27 // ErrLoginAlreadyExists is returned when a Login is added to a profileStore, but another Login with the same
28 // Type and Value already exists in the profileStore.
29 ErrLoginAlreadyExists = errors.New("login already exists in profileStore")
30 // ErrLoginNotFound is returned when a Login is requested but not found in the profileStore.
31 ErrLoginNotFound = errors.New("login not found in profileStore")
33 // ErrMissingPassphrase is returned when a ProfileChange is validated but does not contain a
34 // Passphrase, and requires one.
35 ErrMissingPassphrase = errors.New("missing passphrase")
36 // ErrMissingPassphraseReset is returned when a ProfileChange is validated but does not contain
37 // a PassphraseReset, and requires one.
38 ErrMissingPassphraseReset = errors.New("missing passphrase reset")
39 // ErrMissingPassphraseResetCreated is returned when a ProfileChange is validated but does not
40 // contain a PassphraseResetCreated, and requires one.
41 ErrMissingPassphraseResetCreated = errors.New("missing passphrase reset created timestamp")
42 // ErrPassphraseTooShort is returned when a ProfileChange is validated and contains a Passphrase,
43 // but the Passphrase is shorter than MinPassphraseLength.
44 ErrPassphraseTooShort = errors.New("passphrase too short")
45 // ErrPassphraseTooLong is returned when a ProfileChange is validated and contains a Passphrase,
46 // but the Passphrase is longer than MaxPassphraseLength.
47 ErrPassphraseTooLong = errors.New("passphrase too long")
50 // Profile represents a single user of the service,
51 // including their authentication information, but not
52 // including their username or email.
62 PassphraseReset string
63 PassphraseResetCreated time.Time
68 // ApplyChange applies the properties of the passed ProfileChange
69 // to the Profile it is called on.
70 func (p *Profile) ApplyChange(change ProfileChange) {
71 if change.Name != nil {
74 if change.Passphrase != nil {
75 p.Passphrase = *change.Passphrase
77 if change.Iterations != nil {
78 p.Iterations = *change.Iterations
80 if change.Salt != nil {
83 if change.PassphraseScheme != nil {
84 p.PassphraseScheme = *change.PassphraseScheme
86 if change.Compromised != nil {
87 p.Compromised = *change.Compromised
89 if change.LockedUntil != nil {
90 p.LockedUntil = *change.LockedUntil
92 if change.PassphraseReset != nil {
93 p.PassphraseReset = *change.PassphraseReset
95 if change.PassphraseResetCreated != nil {
96 p.PassphraseResetCreated = *change.PassphraseResetCreated
98 if change.LastSeen != nil {
99 p.LastSeen = *change.LastSeen
103 // ApplyBulkChange applies the properties of the passed BulkProfileChange
104 // to the Profile it is called on.
105 func (p *Profile) ApplyBulkChange(change BulkProfileChange) {
106 if change.Compromised != nil {
107 p.Compromised = *change.Compromised
111 // ProfileChange represents a single atomic change to a Profile's mutable data.
112 type ProfileChange struct {
117 PassphraseScheme *int
119 LockedUntil *time.Time
120 PassphraseReset *string
121 PassphraseResetCreated *time.Time
125 // Validate checks the ProfileChange it is called on
126 // and asserts its internal validity, or lack thereof.
127 // A descriptive error will be returned in the case of
128 // an invalid change.
129 func (c ProfileChange) Validate() error {
130 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 {
131 return ErrEmptyChange
133 if c.PassphraseScheme != nil && c.Passphrase == nil {
134 return ErrMissingPassphrase
136 if c.PassphraseReset != nil && c.PassphraseResetCreated == nil {
137 return ErrMissingPassphraseResetCreated
139 if c.PassphraseReset == nil && c.PassphraseResetCreated != nil {
140 return ErrMissingPassphraseReset
142 if c.Salt != nil && c.Passphrase == nil {
143 return ErrMissingPassphrase
145 if c.Iterations != nil && c.Passphrase == nil {
146 return ErrMissingPassphrase
148 if c.Passphrase != nil && len(*c.Passphrase) < MinPassphraseLength {
149 return ErrPassphraseTooShort
151 if c.Passphrase != nil && len(*c.Passphrase) > MaxPassphraseLength {
152 return ErrPassphraseTooLong
157 // BulkProfileChange represents a single atomic change to many Profiles' mutable data.
158 // It is a subset of a ProfileChange, as it doesn't make sense to mutate some of the
159 // ProfileChange values across many Profiles all at once.
160 type BulkProfileChange struct {
164 // Validate checks the BulkProfileChange it is called on
165 // and asserts its internal validity, or lack thereof.
166 // A descriptive error will be returned in the case of an
168 func (b BulkProfileChange) Validate() error {
169 if b.Compromised == nil {
170 return ErrEmptyChange
175 // Login represents a single human-friendly identifier for
176 // a given Profile that can be used to log into that Profile.
177 // Each Profile may only have one Login for each Type.
186 type profileStore interface {
187 getProfileByID(id uuid.ID) (Profile, error)
188 getProfileByLogin(value string) (Profile, error)
189 saveProfile(profile Profile) error
190 updateProfile(id uuid.ID, change ProfileChange) error
191 updateProfiles(ids []uuid.ID, change BulkProfileChange) error
192 deleteProfile(id uuid.ID) error
194 addLogin(login Login) error
195 removeLogin(value string, profile uuid.ID) error
196 recordLoginUse(value string, when time.Time) error
197 listLogins(profile uuid.ID, num, offset int) ([]Login, error)
200 func (m *memstore) getProfileByID(id uuid.ID) (Profile, error) {
201 m.profileLock.RLock()
202 defer m.profileLock.RUnlock()
203 p, ok := m.profiles[id.String()]
205 return Profile{}, ErrProfileNotFound
210 func (m *memstore) getProfileByLogin(value string) (Profile, error) {
212 defer m.loginLock.RUnlock()
213 login, ok := m.logins[value]
215 return Profile{}, ErrLoginNotFound
217 m.profileLock.RLock()
218 defer m.profileLock.RUnlock()
219 profile, ok := m.profiles[login.ProfileID.String()]
221 return Profile{}, ErrProfileNotFound
226 func (m *memstore) saveProfile(profile Profile) error {
228 defer m.profileLock.Unlock()
229 _, ok := m.profiles[profile.ID.String()]
231 return ErrProfileAlreadyExists
233 m.profiles[profile.ID.String()] = profile
237 func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error {
239 defer m.profileLock.Unlock()
240 p, ok := m.profiles[id.String()]
242 return ErrProfileNotFound
244 p.ApplyChange(change)
245 m.profiles[id.String()] = p
249 func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error {
251 defer m.profileLock.Unlock()
252 for id, profile := range m.profiles {
253 for _, i := range ids {
254 if id == i.String() {
255 profile.ApplyBulkChange(change)
256 m.profiles[id] = profile
264 func (m *memstore) deleteProfile(id uuid.ID) error {
266 defer m.profileLock.Unlock()
267 _, ok := m.profiles[id.String()]
269 return ErrProfileNotFound
271 delete(m.profiles, id.String())
275 func (m *memstore) addLogin(login Login) error {
277 defer m.loginLock.Unlock()
278 _, ok := m.logins[login.Value]
280 return ErrLoginAlreadyExists
282 m.logins[login.Value] = login
283 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Value)
287 func (m *memstore) removeLogin(value string, profile uuid.ID) error {
289 defer m.loginLock.Unlock()
290 l, ok := m.logins[value]
292 return ErrLoginNotFound
294 if !l.ProfileID.Equal(profile) {
295 return ErrLoginNotFound
297 delete(m.logins, value)
299 for p, id := range m.profileLoginLookup[profile.String()] {
306 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...)
311 func (m *memstore) recordLoginUse(value string, when time.Time) error {
313 defer m.loginLock.Unlock()
314 l, ok := m.logins[value]
316 return ErrLoginNotFound
323 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) {
325 defer m.loginLock.RUnlock()
326 ids, ok := m.profileLoginLookup[profile.String()]
328 return []Login{}, nil
330 if len(ids) > num+offset {
331 ids = ids[offset : num+offset]
332 } else if len(ids) > offset {
335 return []Login{}, nil
338 for _, id := range ids {
339 login, ok := m.logins[id]
343 logins = append(logins, login)