auth

Paddy 2014-11-02 Parent:e45bfa2abc00 Child:42bc3e44f4fe

65:f97ca45d5657 Go to Latest

auth/profile.go

Fix bug with response_type redirect, add tests. Test that we redirect with an error when an invalid response_type is supplied. Fix a bug that would not add any of our parameters to the redirect URL.

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