Stub out sessions.
Stop using the Login type when getting profile by Login, removing Logins,
or recording Login use. The Login value has to be unique, anyways, and we don't
actually know the Login type when getting a profile by Login. That's sort of the
point.
Create the concept of Sessions and a sessionStore type to manage our
authentication sessions with the server. As per OWASP, we're basically just
going to use a transparent, SHA256-generated random string as an ID, and store
it client-side and server-side and just pass it back and forth.
Add the ProfileID to the Grant type, because we need to remember who granted
access. That's sort of important.
Set a defaultGrantExpiration constant to an hour, so we have that one constant
when creating new Grants.
Create a helper that pulls the session ID out of an auth cookie, checks it
against the sessionStore, and returns the Session if it's valid.
Create a helper that pulls the username and password out of a basic auth header.
Create a helper that authenticates a user's login and passphrase, checking them
against the profileStore securely.
Stub out how the cookie checking is going to work for getting grant approval.
Fix the stored Grant RedirectURI to be the passed in redirect URI, not the
RedirectURI that we ultimately redirect to. This is in accordance with the spec.
Store the profile ID from our session in the created Grant.
Stub out a GetTokenHandler that will allow users to exchange a Grant for a
Token.
Set a constant for the current passphrase scheme, which we will increment for
each revision to the passphrase scheme, for backwards compatibility.
Change the Profile iterations property to an int, not an int64, to match the
code.secondbit.org/pass library (which is matching the PBKDF2 library).
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)