auth
auth/profile.go
Update CheckEndpoints for strict checking, add CountEndpoints. Create a "strict" mode for CheckEndpoints that will only return true on an exact match, and update the memstore implementation accordingly. Add tests to make sure that the strict mode is adhered to. We need this mode because in certain situations (e.g., the client has more than one endpoint registered), the spec demands a full-string comparison. Add a CountEndpoints method to the ClientStore that will return the number of endpoints registered for a specific client. As we just mentioned, the rules for how a redirect URI is validated depend upon the number of endpoints a client has registered, so we need to be able to get at that number.
| 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@48 | 11 MinPassphraseLength = 6 |
| paddy@48 | 12 MaxPassphraseLength = 64 |
| paddy@48 | 13 ) |
| paddy@48 | 14 |
| paddy@38 | 15 var ( |
| paddy@49 | 16 ErrNoProfileStore = errors.New("no ProfileStore was specified for the Context") |
| paddy@38 | 17 ErrProfileAlreadyExists = errors.New("profile already exists in ProfileStore") |
| paddy@38 | 18 ErrProfileNotFound = errors.New("profile not found in ProfileStore") |
| paddy@44 | 19 ErrLoginAlreadyExists = errors.New("login already exists in ProfileStore") |
| paddy@44 | 20 ErrLoginNotFound = errors.New("login not found in ProfileStore") |
| paddy@48 | 21 |
| paddy@48 | 22 ErrMissingPassphrase = errors.New("missing passphrase") |
| paddy@48 | 23 ErrMissingPassphraseReset = errors.New("missing passphrase reset") |
| paddy@48 | 24 ErrMissingPassphraseResetCreated = errors.New("missing passphrase reset created timestamp") |
| paddy@48 | 25 ErrPassphraseTooShort = errors.New("passphrase too short") |
| paddy@48 | 26 ErrPassphraseTooLong = errors.New("passphrase too long") |
| paddy@38 | 27 ) |
| paddy@38 | 28 |
| paddy@27 | 29 type Profile struct { |
| paddy@38 | 30 ID uuid.ID |
| paddy@38 | 31 Name string |
| paddy@38 | 32 Passphrase string |
| paddy@38 | 33 Iterations int64 |
| paddy@38 | 34 Salt string |
| paddy@38 | 35 PassphraseScheme int |
| paddy@38 | 36 Compromised bool |
| paddy@38 | 37 LockedUntil time.Time |
| paddy@38 | 38 PassphraseReset string |
| paddy@38 | 39 PassphraseResetCreated time.Time |
| paddy@38 | 40 Created time.Time |
| paddy@38 | 41 LastSeen time.Time |
| paddy@38 | 42 } |
| paddy@38 | 43 |
| paddy@38 | 44 func (p *Profile) ApplyChange(change ProfileChange) { |
| paddy@38 | 45 if change.Name != nil { |
| paddy@38 | 46 p.Name = *change.Name |
| paddy@38 | 47 } |
| paddy@38 | 48 if change.Passphrase != nil { |
| paddy@38 | 49 p.Passphrase = *change.Passphrase |
| paddy@38 | 50 } |
| paddy@38 | 51 if change.Iterations != nil { |
| paddy@38 | 52 p.Iterations = *change.Iterations |
| paddy@38 | 53 } |
| paddy@38 | 54 if change.Salt != nil { |
| paddy@38 | 55 p.Salt = *change.Salt |
| paddy@38 | 56 } |
| paddy@38 | 57 if change.PassphraseScheme != nil { |
| paddy@38 | 58 p.PassphraseScheme = *change.PassphraseScheme |
| paddy@38 | 59 } |
| paddy@38 | 60 if change.Compromised != nil { |
| paddy@38 | 61 p.Compromised = *change.Compromised |
| paddy@38 | 62 } |
| paddy@38 | 63 if change.LockedUntil != nil { |
| paddy@38 | 64 p.LockedUntil = *change.LockedUntil |
| paddy@38 | 65 } |
| paddy@38 | 66 if change.PassphraseReset != nil { |
| paddy@38 | 67 p.PassphraseReset = *change.PassphraseReset |
| paddy@38 | 68 } |
| paddy@38 | 69 if change.PassphraseResetCreated != nil { |
| paddy@38 | 70 p.PassphraseResetCreated = *change.PassphraseResetCreated |
| paddy@38 | 71 } |
| paddy@38 | 72 if change.LastSeen != nil { |
| paddy@38 | 73 p.LastSeen = *change.LastSeen |
| paddy@38 | 74 } |
| paddy@38 | 75 } |
| paddy@38 | 76 |
| paddy@44 | 77 func (p *Profile) ApplyBulkChange(change BulkProfileChange) { |
| paddy@44 | 78 if change.Compromised != nil { |
| paddy@44 | 79 p.Compromised = *change.Compromised |
| paddy@44 | 80 } |
| paddy@44 | 81 } |
| paddy@44 | 82 |
| paddy@38 | 83 type ProfileChange struct { |
| paddy@38 | 84 Name *string |
| paddy@38 | 85 Passphrase *string |
| paddy@38 | 86 Iterations *int64 |
| paddy@38 | 87 Salt *string |
| paddy@38 | 88 PassphraseScheme *int |
| paddy@38 | 89 Compromised *bool |
| paddy@38 | 90 LockedUntil *time.Time |
| paddy@38 | 91 PassphraseReset *string |
| paddy@38 | 92 PassphraseResetCreated *time.Time |
| paddy@38 | 93 LastSeen *time.Time |
| paddy@38 | 94 } |
| paddy@38 | 95 |
| paddy@38 | 96 func (c ProfileChange) Validate() error { |
| paddy@48 | 97 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 | 98 return ErrEmptyChange |
| paddy@48 | 99 } |
| paddy@48 | 100 if c.PassphraseScheme != nil && c.Passphrase == nil { |
| paddy@48 | 101 return ErrMissingPassphrase |
| paddy@48 | 102 } |
| paddy@48 | 103 if c.PassphraseReset != nil && c.PassphraseResetCreated == nil { |
| paddy@48 | 104 return ErrMissingPassphraseResetCreated |
| paddy@48 | 105 } |
| paddy@48 | 106 if c.PassphraseReset == nil && c.PassphraseResetCreated != nil { |
| paddy@48 | 107 return ErrMissingPassphraseReset |
| paddy@48 | 108 } |
| paddy@48 | 109 if c.Salt != nil && c.Passphrase == nil { |
| paddy@48 | 110 return ErrMissingPassphrase |
| paddy@48 | 111 } |
| paddy@48 | 112 if c.Iterations != nil && c.Passphrase == nil { |
| paddy@48 | 113 return ErrMissingPassphrase |
| paddy@48 | 114 } |
| paddy@48 | 115 if c.Passphrase != nil && len(*c.Passphrase) < MinPassphraseLength { |
| paddy@48 | 116 return ErrPassphraseTooShort |
| paddy@48 | 117 } |
| paddy@48 | 118 if c.Passphrase != nil && len(*c.Passphrase) > MaxPassphraseLength { |
| paddy@48 | 119 return ErrPassphraseTooLong |
| paddy@48 | 120 } |
| paddy@38 | 121 return nil |
| paddy@27 | 122 } |
| paddy@27 | 123 |
| paddy@44 | 124 type BulkProfileChange struct { |
| paddy@44 | 125 Compromised *bool |
| paddy@44 | 126 } |
| paddy@44 | 127 |
| paddy@44 | 128 func (b BulkProfileChange) Validate() error { |
| paddy@48 | 129 if b.Compromised == nil { |
| paddy@48 | 130 return ErrEmptyChange |
| paddy@48 | 131 } |
| paddy@44 | 132 return nil |
| paddy@44 | 133 } |
| paddy@44 | 134 |
| paddy@27 | 135 type Login struct { |
| paddy@27 | 136 Type string |
| paddy@27 | 137 Value string |
| paddy@27 | 138 ProfileID uuid.ID |
| paddy@27 | 139 Created time.Time |
| paddy@27 | 140 LastUsed time.Time |
| paddy@27 | 141 } |
| paddy@27 | 142 |
| paddy@27 | 143 type ProfileStore interface { |
| paddy@27 | 144 GetProfileByID(id uuid.ID) (Profile, error) |
| paddy@44 | 145 GetProfileByLogin(loginType, value string) (Profile, error) |
| paddy@38 | 146 SaveProfile(profile Profile) error |
| paddy@38 | 147 UpdateProfile(id uuid.ID, change ProfileChange) error |
| paddy@44 | 148 UpdateProfiles(ids []uuid.ID, change BulkProfileChange) error |
| paddy@27 | 149 DeleteProfile(id uuid.ID) error |
| paddy@44 | 150 |
| paddy@44 | 151 AddLogin(login Login) error |
| paddy@44 | 152 RemoveLogin(loginType, value string, profile uuid.ID) error |
| paddy@44 | 153 RecordLoginUse(loginType, value string, when time.Time) error |
| paddy@44 | 154 ListLogins(profile uuid.ID, num, offset int) ([]Login, error) |
| paddy@38 | 155 } |
| paddy@27 | 156 |
| paddy@38 | 157 func (m *Memstore) GetProfileByID(id uuid.ID) (Profile, error) { |
| paddy@38 | 158 m.profileLock.RLock() |
| paddy@38 | 159 defer m.profileLock.RUnlock() |
| paddy@38 | 160 p, ok := m.profiles[id.String()] |
| paddy@38 | 161 if !ok { |
| paddy@38 | 162 return Profile{}, ErrProfileNotFound |
| paddy@38 | 163 } |
| paddy@38 | 164 return p, nil |
| paddy@27 | 165 } |
| paddy@38 | 166 |
| paddy@44 | 167 func (m *Memstore) GetProfileByLogin(loginType, value string) (Profile, error) { |
| paddy@44 | 168 m.loginLock.RLock() |
| paddy@44 | 169 defer m.loginLock.RUnlock() |
| paddy@44 | 170 login, ok := m.logins[loginType+":"+value] |
| paddy@44 | 171 if !ok { |
| paddy@44 | 172 return Profile{}, ErrLoginNotFound |
| paddy@44 | 173 } |
| paddy@44 | 174 m.profileLock.RLock() |
| paddy@44 | 175 defer m.profileLock.RUnlock() |
| paddy@44 | 176 profile, ok := m.profiles[login.ProfileID.String()] |
| paddy@44 | 177 if !ok { |
| paddy@44 | 178 return Profile{}, ErrProfileNotFound |
| paddy@44 | 179 } |
| paddy@44 | 180 return profile, nil |
| paddy@38 | 181 } |
| paddy@38 | 182 |
| paddy@38 | 183 func (m *Memstore) SaveProfile(profile Profile) error { |
| paddy@38 | 184 m.profileLock.Lock() |
| paddy@38 | 185 defer m.profileLock.Unlock() |
| paddy@38 | 186 _, ok := m.profiles[profile.ID.String()] |
| paddy@38 | 187 if ok { |
| paddy@38 | 188 return ErrProfileAlreadyExists |
| paddy@38 | 189 } |
| paddy@38 | 190 m.profiles[profile.ID.String()] = profile |
| paddy@38 | 191 return nil |
| paddy@38 | 192 } |
| paddy@38 | 193 |
| paddy@38 | 194 func (m *Memstore) UpdateProfile(id uuid.ID, change ProfileChange) error { |
| paddy@38 | 195 m.profileLock.Lock() |
| paddy@38 | 196 defer m.profileLock.Unlock() |
| paddy@38 | 197 p, ok := m.profiles[id.String()] |
| paddy@38 | 198 if !ok { |
| paddy@38 | 199 return ErrProfileNotFound |
| paddy@38 | 200 } |
| paddy@38 | 201 p.ApplyChange(change) |
| paddy@38 | 202 m.profiles[id.String()] = p |
| paddy@38 | 203 return nil |
| paddy@38 | 204 } |
| paddy@38 | 205 |
| paddy@44 | 206 func (m *Memstore) UpdateProfiles(ids []uuid.ID, change BulkProfileChange) error { |
| paddy@44 | 207 m.profileLock.Lock() |
| paddy@44 | 208 defer m.profileLock.Unlock() |
| paddy@44 | 209 for id, profile := range m.profiles { |
| paddy@44 | 210 for _, i := range ids { |
| paddy@44 | 211 if id == i.String() { |
| paddy@44 | 212 profile.ApplyBulkChange(change) |
| paddy@44 | 213 m.profiles[id] = profile |
| paddy@44 | 214 break |
| paddy@44 | 215 } |
| paddy@44 | 216 } |
| paddy@44 | 217 } |
| paddy@44 | 218 return nil |
| paddy@44 | 219 } |
| paddy@44 | 220 |
| paddy@38 | 221 func (m *Memstore) DeleteProfile(id uuid.ID) error { |
| paddy@38 | 222 m.profileLock.Lock() |
| paddy@38 | 223 defer m.profileLock.Unlock() |
| paddy@38 | 224 _, ok := m.profiles[id.String()] |
| paddy@38 | 225 if !ok { |
| paddy@38 | 226 return ErrProfileNotFound |
| paddy@38 | 227 } |
| paddy@38 | 228 delete(m.profiles, id.String()) |
| paddy@38 | 229 return nil |
| paddy@38 | 230 } |
| paddy@40 | 231 |
| paddy@44 | 232 func (m *Memstore) AddLogin(login Login) error { |
| paddy@44 | 233 m.loginLock.Lock() |
| paddy@44 | 234 defer m.loginLock.Unlock() |
| paddy@44 | 235 _, ok := m.logins[login.Type+":"+login.Value] |
| paddy@44 | 236 if ok { |
| paddy@44 | 237 return ErrLoginAlreadyExists |
| paddy@44 | 238 } |
| paddy@44 | 239 m.logins[login.Type+":"+login.Value] = login |
| paddy@44 | 240 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Type+":"+login.Value) |
| paddy@44 | 241 return nil |
| paddy@44 | 242 } |
| paddy@44 | 243 |
| paddy@44 | 244 func (m *Memstore) RemoveLogin(loginType, value string, profile uuid.ID) error { |
| paddy@44 | 245 m.loginLock.Lock() |
| paddy@44 | 246 defer m.loginLock.Unlock() |
| paddy@44 | 247 l, ok := m.logins[loginType+":"+value] |
| paddy@44 | 248 if !ok { |
| paddy@44 | 249 return ErrLoginNotFound |
| paddy@44 | 250 } |
| paddy@44 | 251 if !l.ProfileID.Equal(profile) { |
| paddy@44 | 252 return ErrLoginNotFound |
| paddy@44 | 253 } |
| paddy@44 | 254 delete(m.logins, loginType+":"+value) |
| paddy@44 | 255 pos := -1 |
| paddy@44 | 256 for p, id := range m.profileLoginLookup[profile.String()] { |
| paddy@44 | 257 if id == loginType+":"+value { |
| paddy@44 | 258 pos = p |
| paddy@44 | 259 break |
| paddy@44 | 260 } |
| paddy@44 | 261 } |
| paddy@44 | 262 if pos >= 0 { |
| paddy@44 | 263 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...) |
| paddy@44 | 264 } |
| paddy@44 | 265 return nil |
| paddy@44 | 266 } |
| paddy@44 | 267 |
| paddy@44 | 268 func (m *Memstore) RecordLoginUse(loginType, value string, when time.Time) error { |
| paddy@44 | 269 m.loginLock.Lock() |
| paddy@44 | 270 defer m.loginLock.Unlock() |
| paddy@44 | 271 l, ok := m.logins[loginType+":"+value] |
| paddy@44 | 272 if !ok { |
| paddy@44 | 273 return ErrLoginNotFound |
| paddy@44 | 274 } |
| paddy@44 | 275 l.LastUsed = when |
| paddy@44 | 276 m.logins[loginType+":"+value] = l |
| paddy@44 | 277 return nil |
| paddy@44 | 278 } |
| paddy@44 | 279 |
| paddy@44 | 280 func (m *Memstore) ListLogins(profile uuid.ID, num, offset int) ([]Login, error) { |
| paddy@44 | 281 m.loginLock.RLock() |
| paddy@44 | 282 defer m.loginLock.RUnlock() |
| paddy@44 | 283 ids, ok := m.profileLoginLookup[profile.String()] |
| paddy@44 | 284 if !ok { |
| paddy@44 | 285 return []Login{}, nil |
| paddy@44 | 286 } |
| paddy@44 | 287 if len(ids) > num+offset { |
| paddy@44 | 288 ids = ids[offset : num+offset] |
| paddy@44 | 289 } else if len(ids) > offset { |
| paddy@44 | 290 ids = ids[offset:] |
| paddy@44 | 291 } else { |
| paddy@44 | 292 return []Login{}, nil |
| paddy@44 | 293 } |
| paddy@44 | 294 logins := []Login{} |
| paddy@44 | 295 for _, id := range ids { |
| paddy@44 | 296 login, ok := m.logins[id] |
| paddy@44 | 297 if !ok { |
| paddy@44 | 298 continue |
| paddy@44 | 299 } |
| paddy@44 | 300 logins = append(logins, login) |
| paddy@44 | 301 } |
| paddy@44 | 302 return logins, nil |
| paddy@44 | 303 } |