auth
auth/profile.go
More tests, login redirect bugfix. Add tests for our cookie checking helper and our helper for generating login redirection URIs. Fix a bug where the URL to redirect to was being URL-encoded twice when included in the login redirect URI.
| paddy@27 | 1 package auth |
| paddy@27 | 2 |
| paddy@27 | 3 import ( |
| paddy@38 | 4 "errors" |
| paddy@27 | 5 "time" |
| paddy@27 | 6 |
| paddy@45 | 7 "code.secondbit.org/uuid" |
| paddy@27 | 8 ) |
| paddy@27 | 9 |
| paddy@48 | 10 const ( |
| paddy@57 | 11 // MinPassphraseLength is the minimum length, in bytes, of a passphrase, exclusive. |
| paddy@48 | 12 MinPassphraseLength = 6 |
| paddy@57 | 13 // MaxPassphraseLength is the maximum length, in bytes, of a passphrase, exclusive. |
| paddy@48 | 14 MaxPassphraseLength = 64 |
| paddy@69 | 15 // CurPassphraseScheme is the current passphrase scheme. Incrememnt it when we use a different passphrase scheme |
| paddy@69 | 16 CurPassphraseScheme = 1 |
| paddy@48 | 17 ) |
| paddy@48 | 18 |
| paddy@38 | 19 var ( |
| paddy@57 | 20 // ErrNoProfileStore is returned when a Context tries to act on a profileStore without setting one first. |
| paddy@57 | 21 ErrNoProfileStore = errors.New("no profileStore was specified for the Context") |
| paddy@57 | 22 // ErrProfileAlreadyExists is returned when a Profile is added to a profileStore, but another Profile with |
| paddy@57 | 23 // the same ID already exists in the profileStore. |
| paddy@57 | 24 ErrProfileAlreadyExists = errors.New("profile already exists in profileStore") |
| paddy@57 | 25 // ErrProfileNotFound is returned when a Profile is requested but not found in the profileStore. |
| paddy@57 | 26 ErrProfileNotFound = errors.New("profile not found in profileStore") |
| paddy@57 | 27 // ErrLoginAlreadyExists is returned when a Login is added to a profileStore, but another Login with the same |
| paddy@57 | 28 // Type and Value already exists in the profileStore. |
| paddy@57 | 29 ErrLoginAlreadyExists = errors.New("login already exists in profileStore") |
| paddy@57 | 30 // ErrLoginNotFound is returned when a Login is requested but not found in the profileStore. |
| paddy@57 | 31 ErrLoginNotFound = errors.New("login not found in profileStore") |
| paddy@48 | 32 |
| paddy@57 | 33 // ErrMissingPassphrase is returned when a ProfileChange is validated but does not contain a |
| paddy@57 | 34 // Passphrase, and requires one. |
| paddy@57 | 35 ErrMissingPassphrase = errors.New("missing passphrase") |
| paddy@57 | 36 // ErrMissingPassphraseReset is returned when a ProfileChange is validated but does not contain |
| paddy@57 | 37 // a PassphraseReset, and requires one. |
| paddy@57 | 38 ErrMissingPassphraseReset = errors.New("missing passphrase reset") |
| paddy@57 | 39 // ErrMissingPassphraseResetCreated is returned when a ProfileChange is validated but does not |
| paddy@57 | 40 // contain a PassphraseResetCreated, and requires one. |
| paddy@48 | 41 ErrMissingPassphraseResetCreated = errors.New("missing passphrase reset created timestamp") |
| paddy@57 | 42 // ErrPassphraseTooShort is returned when a ProfileChange is validated and contains a Passphrase, |
| paddy@57 | 43 // but the Passphrase is shorter than MinPassphraseLength. |
| paddy@57 | 44 ErrPassphraseTooShort = errors.New("passphrase too short") |
| paddy@57 | 45 // ErrPassphraseTooLong is returned when a ProfileChange is validated and contains a Passphrase, |
| paddy@57 | 46 // but the Passphrase is longer than MaxPassphraseLength. |
| paddy@57 | 47 ErrPassphraseTooLong = errors.New("passphrase too long") |
| paddy@38 | 48 ) |
| paddy@38 | 49 |
| paddy@57 | 50 // Profile represents a single user of the service, |
| paddy@57 | 51 // including their authentication information, but not |
| paddy@57 | 52 // including their username or email. |
| paddy@27 | 53 type Profile struct { |
| paddy@38 | 54 ID uuid.ID |
| paddy@38 | 55 Name string |
| paddy@38 | 56 Passphrase string |
| paddy@69 | 57 Iterations int |
| paddy@38 | 58 Salt string |
| paddy@38 | 59 PassphraseScheme int |
| paddy@38 | 60 Compromised bool |
| paddy@38 | 61 LockedUntil time.Time |
| paddy@38 | 62 PassphraseReset string |
| paddy@38 | 63 PassphraseResetCreated time.Time |
| paddy@38 | 64 Created time.Time |
| paddy@38 | 65 LastSeen time.Time |
| paddy@38 | 66 } |
| paddy@38 | 67 |
| paddy@57 | 68 // ApplyChange applies the properties of the passed ProfileChange |
| paddy@57 | 69 // to the Profile it is called on. |
| paddy@38 | 70 func (p *Profile) ApplyChange(change ProfileChange) { |
| paddy@38 | 71 if change.Name != nil { |
| paddy@38 | 72 p.Name = *change.Name |
| paddy@38 | 73 } |
| paddy@38 | 74 if change.Passphrase != nil { |
| paddy@38 | 75 p.Passphrase = *change.Passphrase |
| paddy@38 | 76 } |
| paddy@38 | 77 if change.Iterations != nil { |
| paddy@38 | 78 p.Iterations = *change.Iterations |
| paddy@38 | 79 } |
| paddy@38 | 80 if change.Salt != nil { |
| paddy@38 | 81 p.Salt = *change.Salt |
| paddy@38 | 82 } |
| paddy@38 | 83 if change.PassphraseScheme != nil { |
| paddy@38 | 84 p.PassphraseScheme = *change.PassphraseScheme |
| paddy@38 | 85 } |
| paddy@38 | 86 if change.Compromised != nil { |
| paddy@38 | 87 p.Compromised = *change.Compromised |
| paddy@38 | 88 } |
| paddy@38 | 89 if change.LockedUntil != nil { |
| paddy@38 | 90 p.LockedUntil = *change.LockedUntil |
| paddy@38 | 91 } |
| paddy@38 | 92 if change.PassphraseReset != nil { |
| paddy@38 | 93 p.PassphraseReset = *change.PassphraseReset |
| paddy@38 | 94 } |
| paddy@38 | 95 if change.PassphraseResetCreated != nil { |
| paddy@38 | 96 p.PassphraseResetCreated = *change.PassphraseResetCreated |
| paddy@38 | 97 } |
| paddy@38 | 98 if change.LastSeen != nil { |
| paddy@38 | 99 p.LastSeen = *change.LastSeen |
| paddy@38 | 100 } |
| paddy@38 | 101 } |
| paddy@38 | 102 |
| paddy@57 | 103 // ApplyBulkChange applies the properties of the passed BulkProfileChange |
| paddy@57 | 104 // to the Profile it is called on. |
| paddy@44 | 105 func (p *Profile) ApplyBulkChange(change BulkProfileChange) { |
| paddy@44 | 106 if change.Compromised != nil { |
| paddy@44 | 107 p.Compromised = *change.Compromised |
| paddy@44 | 108 } |
| paddy@44 | 109 } |
| paddy@44 | 110 |
| paddy@57 | 111 // ProfileChange represents a single atomic change to a Profile's mutable data. |
| paddy@38 | 112 type ProfileChange struct { |
| paddy@38 | 113 Name *string |
| paddy@38 | 114 Passphrase *string |
| paddy@69 | 115 Iterations *int |
| paddy@38 | 116 Salt *string |
| paddy@38 | 117 PassphraseScheme *int |
| paddy@38 | 118 Compromised *bool |
| paddy@38 | 119 LockedUntil *time.Time |
| paddy@38 | 120 PassphraseReset *string |
| paddy@38 | 121 PassphraseResetCreated *time.Time |
| paddy@38 | 122 LastSeen *time.Time |
| paddy@38 | 123 } |
| paddy@38 | 124 |
| paddy@57 | 125 // Validate checks the ProfileChange it is called on |
| paddy@57 | 126 // and asserts its internal validity, or lack thereof. |
| paddy@57 | 127 // A descriptive error will be returned in the case of |
| paddy@57 | 128 // an invalid change. |
| paddy@38 | 129 func (c ProfileChange) Validate() error { |
| paddy@48 | 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 { |
| paddy@48 | 131 return ErrEmptyChange |
| paddy@48 | 132 } |
| paddy@48 | 133 if c.PassphraseScheme != nil && c.Passphrase == nil { |
| paddy@48 | 134 return ErrMissingPassphrase |
| paddy@48 | 135 } |
| paddy@48 | 136 if c.PassphraseReset != nil && c.PassphraseResetCreated == nil { |
| paddy@48 | 137 return ErrMissingPassphraseResetCreated |
| paddy@48 | 138 } |
| paddy@48 | 139 if c.PassphraseReset == nil && c.PassphraseResetCreated != nil { |
| paddy@48 | 140 return ErrMissingPassphraseReset |
| paddy@48 | 141 } |
| paddy@48 | 142 if c.Salt != nil && c.Passphrase == nil { |
| paddy@48 | 143 return ErrMissingPassphrase |
| paddy@48 | 144 } |
| paddy@48 | 145 if c.Iterations != nil && c.Passphrase == nil { |
| paddy@48 | 146 return ErrMissingPassphrase |
| paddy@48 | 147 } |
| paddy@48 | 148 if c.Passphrase != nil && len(*c.Passphrase) < MinPassphraseLength { |
| paddy@48 | 149 return ErrPassphraseTooShort |
| paddy@48 | 150 } |
| paddy@48 | 151 if c.Passphrase != nil && len(*c.Passphrase) > MaxPassphraseLength { |
| paddy@48 | 152 return ErrPassphraseTooLong |
| paddy@48 | 153 } |
| paddy@38 | 154 return nil |
| paddy@27 | 155 } |
| paddy@27 | 156 |
| paddy@57 | 157 // BulkProfileChange represents a single atomic change to many Profiles' mutable data. |
| paddy@57 | 158 // It is a subset of a ProfileChange, as it doesn't make sense to mutate some of the |
| paddy@57 | 159 // ProfileChange values across many Profiles all at once. |
| paddy@44 | 160 type BulkProfileChange struct { |
| paddy@44 | 161 Compromised *bool |
| paddy@44 | 162 } |
| paddy@44 | 163 |
| paddy@57 | 164 // Validate checks the BulkProfileChange it is called on |
| paddy@57 | 165 // and asserts its internal validity, or lack thereof. |
| paddy@57 | 166 // A descriptive error will be returned in the case of an |
| paddy@57 | 167 // invalid change. |
| paddy@44 | 168 func (b BulkProfileChange) Validate() error { |
| paddy@48 | 169 if b.Compromised == nil { |
| paddy@48 | 170 return ErrEmptyChange |
| paddy@48 | 171 } |
| paddy@44 | 172 return nil |
| paddy@44 | 173 } |
| paddy@44 | 174 |
| paddy@57 | 175 // Login represents a single human-friendly identifier for |
| paddy@57 | 176 // a given Profile that can be used to log into that Profile. |
| paddy@57 | 177 // Each Profile may only have one Login for each Type. |
| paddy@27 | 178 type Login struct { |
| paddy@27 | 179 Type string |
| paddy@27 | 180 Value string |
| paddy@27 | 181 ProfileID uuid.ID |
| paddy@27 | 182 Created time.Time |
| paddy@27 | 183 LastUsed time.Time |
| paddy@27 | 184 } |
| paddy@27 | 185 |
| paddy@57 | 186 type profileStore interface { |
| paddy@57 | 187 getProfileByID(id uuid.ID) (Profile, error) |
| paddy@69 | 188 getProfileByLogin(value string) (Profile, error) |
| paddy@57 | 189 saveProfile(profile Profile) error |
| paddy@57 | 190 updateProfile(id uuid.ID, change ProfileChange) error |
| paddy@57 | 191 updateProfiles(ids []uuid.ID, change BulkProfileChange) error |
| paddy@57 | 192 deleteProfile(id uuid.ID) error |
| paddy@44 | 193 |
| paddy@57 | 194 addLogin(login Login) error |
| paddy@69 | 195 removeLogin(value string, profile uuid.ID) error |
| paddy@69 | 196 recordLoginUse(value string, when time.Time) error |
| paddy@57 | 197 listLogins(profile uuid.ID, num, offset int) ([]Login, error) |
| paddy@38 | 198 } |
| paddy@27 | 199 |
| paddy@57 | 200 func (m *memstore) getProfileByID(id uuid.ID) (Profile, error) { |
| paddy@38 | 201 m.profileLock.RLock() |
| paddy@38 | 202 defer m.profileLock.RUnlock() |
| paddy@38 | 203 p, ok := m.profiles[id.String()] |
| paddy@38 | 204 if !ok { |
| paddy@38 | 205 return Profile{}, ErrProfileNotFound |
| paddy@38 | 206 } |
| paddy@38 | 207 return p, nil |
| paddy@27 | 208 } |
| paddy@38 | 209 |
| paddy@69 | 210 func (m *memstore) getProfileByLogin(value string) (Profile, error) { |
| paddy@44 | 211 m.loginLock.RLock() |
| paddy@44 | 212 defer m.loginLock.RUnlock() |
| paddy@69 | 213 login, ok := m.logins[value] |
| paddy@44 | 214 if !ok { |
| paddy@44 | 215 return Profile{}, ErrLoginNotFound |
| paddy@44 | 216 } |
| paddy@44 | 217 m.profileLock.RLock() |
| paddy@44 | 218 defer m.profileLock.RUnlock() |
| paddy@44 | 219 profile, ok := m.profiles[login.ProfileID.String()] |
| paddy@44 | 220 if !ok { |
| paddy@44 | 221 return Profile{}, ErrProfileNotFound |
| paddy@44 | 222 } |
| paddy@44 | 223 return profile, nil |
| paddy@38 | 224 } |
| paddy@38 | 225 |
| paddy@57 | 226 func (m *memstore) saveProfile(profile Profile) error { |
| paddy@38 | 227 m.profileLock.Lock() |
| paddy@38 | 228 defer m.profileLock.Unlock() |
| paddy@38 | 229 _, ok := m.profiles[profile.ID.String()] |
| paddy@38 | 230 if ok { |
| paddy@38 | 231 return ErrProfileAlreadyExists |
| paddy@38 | 232 } |
| paddy@38 | 233 m.profiles[profile.ID.String()] = profile |
| paddy@38 | 234 return nil |
| paddy@38 | 235 } |
| paddy@38 | 236 |
| paddy@57 | 237 func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error { |
| paddy@38 | 238 m.profileLock.Lock() |
| paddy@38 | 239 defer m.profileLock.Unlock() |
| paddy@38 | 240 p, ok := m.profiles[id.String()] |
| paddy@38 | 241 if !ok { |
| paddy@38 | 242 return ErrProfileNotFound |
| paddy@38 | 243 } |
| paddy@38 | 244 p.ApplyChange(change) |
| paddy@38 | 245 m.profiles[id.String()] = p |
| paddy@38 | 246 return nil |
| paddy@38 | 247 } |
| paddy@38 | 248 |
| paddy@57 | 249 func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error { |
| paddy@44 | 250 m.profileLock.Lock() |
| paddy@44 | 251 defer m.profileLock.Unlock() |
| paddy@44 | 252 for id, profile := range m.profiles { |
| paddy@44 | 253 for _, i := range ids { |
| paddy@44 | 254 if id == i.String() { |
| paddy@44 | 255 profile.ApplyBulkChange(change) |
| paddy@44 | 256 m.profiles[id] = profile |
| paddy@44 | 257 break |
| paddy@44 | 258 } |
| paddy@44 | 259 } |
| paddy@44 | 260 } |
| paddy@44 | 261 return nil |
| paddy@44 | 262 } |
| paddy@44 | 263 |
| paddy@57 | 264 func (m *memstore) deleteProfile(id uuid.ID) error { |
| paddy@38 | 265 m.profileLock.Lock() |
| paddy@38 | 266 defer m.profileLock.Unlock() |
| paddy@38 | 267 _, ok := m.profiles[id.String()] |
| paddy@38 | 268 if !ok { |
| paddy@38 | 269 return ErrProfileNotFound |
| paddy@38 | 270 } |
| paddy@38 | 271 delete(m.profiles, id.String()) |
| paddy@38 | 272 return nil |
| paddy@38 | 273 } |
| paddy@40 | 274 |
| paddy@57 | 275 func (m *memstore) addLogin(login Login) error { |
| paddy@44 | 276 m.loginLock.Lock() |
| paddy@44 | 277 defer m.loginLock.Unlock() |
| paddy@69 | 278 _, ok := m.logins[login.Value] |
| paddy@44 | 279 if ok { |
| paddy@44 | 280 return ErrLoginAlreadyExists |
| paddy@44 | 281 } |
| paddy@69 | 282 m.logins[login.Value] = login |
| paddy@69 | 283 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Value) |
| paddy@44 | 284 return nil |
| paddy@44 | 285 } |
| paddy@44 | 286 |
| paddy@69 | 287 func (m *memstore) removeLogin(value string, profile uuid.ID) error { |
| paddy@44 | 288 m.loginLock.Lock() |
| paddy@44 | 289 defer m.loginLock.Unlock() |
| paddy@69 | 290 l, ok := m.logins[value] |
| paddy@44 | 291 if !ok { |
| paddy@44 | 292 return ErrLoginNotFound |
| paddy@44 | 293 } |
| paddy@44 | 294 if !l.ProfileID.Equal(profile) { |
| paddy@44 | 295 return ErrLoginNotFound |
| paddy@44 | 296 } |
| paddy@69 | 297 delete(m.logins, value) |
| paddy@44 | 298 pos := -1 |
| paddy@44 | 299 for p, id := range m.profileLoginLookup[profile.String()] { |
| paddy@69 | 300 if id == value { |
| paddy@44 | 301 pos = p |
| paddy@44 | 302 break |
| paddy@44 | 303 } |
| paddy@44 | 304 } |
| paddy@44 | 305 if pos >= 0 { |
| paddy@44 | 306 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...) |
| paddy@44 | 307 } |
| paddy@44 | 308 return nil |
| paddy@44 | 309 } |
| paddy@44 | 310 |
| paddy@69 | 311 func (m *memstore) recordLoginUse(value string, when time.Time) error { |
| paddy@44 | 312 m.loginLock.Lock() |
| paddy@44 | 313 defer m.loginLock.Unlock() |
| paddy@69 | 314 l, ok := m.logins[value] |
| paddy@44 | 315 if !ok { |
| paddy@44 | 316 return ErrLoginNotFound |
| paddy@44 | 317 } |
| paddy@44 | 318 l.LastUsed = when |
| paddy@69 | 319 m.logins[value] = l |
| paddy@44 | 320 return nil |
| paddy@44 | 321 } |
| paddy@44 | 322 |
| paddy@57 | 323 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) { |
| paddy@44 | 324 m.loginLock.RLock() |
| paddy@44 | 325 defer m.loginLock.RUnlock() |
| paddy@44 | 326 ids, ok := m.profileLoginLookup[profile.String()] |
| paddy@44 | 327 if !ok { |
| paddy@44 | 328 return []Login{}, nil |
| paddy@44 | 329 } |
| paddy@44 | 330 if len(ids) > num+offset { |
| paddy@44 | 331 ids = ids[offset : num+offset] |
| paddy@44 | 332 } else if len(ids) > offset { |
| paddy@44 | 333 ids = ids[offset:] |
| paddy@44 | 334 } else { |
| paddy@44 | 335 return []Login{}, nil |
| paddy@44 | 336 } |
| paddy@44 | 337 logins := []Login{} |
| paddy@44 | 338 for _, id := range ids { |
| paddy@44 | 339 login, ok := m.logins[id] |
| paddy@44 | 340 if !ok { |
| paddy@44 | 341 continue |
| paddy@44 | 342 } |
| paddy@44 | 343 logins = append(logins, login) |
| paddy@44 | 344 } |
| paddy@44 | 345 return logins, nil |
| paddy@44 | 346 } |