auth

Paddy 2015-07-18 Parent:7bba108d2d9a Child:b7e685839a1b

180:4b68bac597b7 Go to Latest

auth/profile.go

Update client to detect errors. The client doesn't treat non-200 responses as errors automatically, so we need to detect when the response.Errors property is set, and use that to return an error. To avoid the boilerplate and an extensive error system, I just wrapped them in an httpErrors type that implements the error interface. That way the errors can be returned, and callers can type-cast and interrogate them. I also updated the GetLogin function to return an auth.ErrLoginNotFound error when the httpErrors response indicates that's the reason the request failed.

History
paddy@27 1 package auth
paddy@27 2
paddy@27 3 import (
paddy@99 4 "encoding/json"
paddy@38 5 "errors"
paddy@149 6 "log"
paddy@99 7 "net/http"
paddy@99 8 "regexp"
paddy@99 9 "strings"
paddy@27 10 "time"
paddy@27 11
paddy@178 12 "code.secondbit.org/events.hg"
paddy@107 13 "code.secondbit.org/uuid.hg"
paddy@178 14
paddy@105 15 "github.com/gorilla/mux"
paddy@27 16 )
paddy@27 17
paddy@48 18 const (
paddy@57 19 // MinPassphraseLength is the minimum length, in bytes, of a passphrase, exclusive.
paddy@48 20 MinPassphraseLength = 6
paddy@57 21 // MaxPassphraseLength is the maximum length, in bytes, of a passphrase, exclusive.
paddy@48 22 MaxPassphraseLength = 64
paddy@69 23 // CurPassphraseScheme is the current passphrase scheme. Incrememnt it when we use a different passphrase scheme
paddy@69 24 CurPassphraseScheme = 1
paddy@99 25 // MaxNameLength is the maximum length, in bytes, of a name, exclusive.
paddy@99 26 MaxNameLength = 64
paddy@99 27 // MaxEmailLength is the maximum length, in bytes, of an email address, exclusive.
paddy@99 28 MaxEmailLength = 64
paddy@178 29
paddy@178 30 // ActionResendVerification is the action property of the event sent when the user wants to resend their login verification code.
paddy@178 31 ActionResendVerification = "resend_verification"
paddy@179 32 ActionLoginVerified = "login_verified"
paddy@48 33 )
paddy@48 34
paddy@38 35 var (
paddy@57 36 // ErrNoProfileStore is returned when a Context tries to act on a profileStore without setting one first.
paddy@57 37 ErrNoProfileStore = errors.New("no profileStore was specified for the Context")
paddy@57 38 // ErrProfileAlreadyExists is returned when a Profile is added to a profileStore, but another Profile with
paddy@57 39 // the same ID already exists in the profileStore.
paddy@57 40 ErrProfileAlreadyExists = errors.New("profile already exists in profileStore")
paddy@57 41 // ErrProfileNotFound is returned when a Profile is requested but not found in the profileStore.
paddy@57 42 ErrProfileNotFound = errors.New("profile not found in profileStore")
paddy@57 43 // ErrLoginAlreadyExists is returned when a Login is added to a profileStore, but another Login with the same
paddy@57 44 // Type and Value already exists in the profileStore.
paddy@57 45 ErrLoginAlreadyExists = errors.New("login already exists in profileStore")
paddy@57 46 // ErrLoginNotFound is returned when a Login is requested but not found in the profileStore.
paddy@57 47 ErrLoginNotFound = errors.New("login not found in profileStore")
paddy@172 48 // ErrLoginVerificationInvalid is returned when a Login is verified with the wrong verification code.
paddy@172 49 ErrLoginVerificationInvalid = errors.New("login verification code incorrect")
paddy@48 50
paddy@57 51 // ErrMissingPassphrase is returned when a ProfileChange is validated but does not contain a
paddy@57 52 // Passphrase, and requires one.
paddy@57 53 ErrMissingPassphrase = errors.New("missing passphrase")
paddy@57 54 // ErrMissingPassphraseReset is returned when a ProfileChange is validated but does not contain
paddy@57 55 // a PassphraseReset, and requires one.
paddy@57 56 ErrMissingPassphraseReset = errors.New("missing passphrase reset")
paddy@57 57 // ErrMissingPassphraseResetCreated is returned when a ProfileChange is validated but does not
paddy@57 58 // contain a PassphraseResetCreated, and requires one.
paddy@48 59 ErrMissingPassphraseResetCreated = errors.New("missing passphrase reset created timestamp")
paddy@57 60 // ErrPassphraseTooShort is returned when a ProfileChange is validated and contains a Passphrase,
paddy@57 61 // but the Passphrase is shorter than MinPassphraseLength.
paddy@57 62 ErrPassphraseTooShort = errors.New("passphrase too short")
paddy@57 63 // ErrPassphraseTooLong is returned when a ProfileChange is validated and contains a Passphrase,
paddy@57 64 // but the Passphrase is longer than MaxPassphraseLength.
paddy@57 65 ErrPassphraseTooLong = errors.New("passphrase too long")
paddy@99 66
paddy@99 67 // ErrProfileCompromised is returned when a user tries to log in with a profile that is suspected
paddy@99 68 // of being compromised.
paddy@99 69 ErrProfileCompromised = errors.New("profile compromised")
paddy@99 70 // ErrProfileLocked is returned when a user tries to log in with a profile that is locked for a certain
paddy@99 71 // duration, to prevent brute force attacks.
paddy@99 72 ErrProfileLocked = errors.New("profile locked")
paddy@173 73
paddy@173 74 ScopeLoginAdmin = Scope{ID: "login_admin", Name: "Administer Logins", Description: "Read and write logins, bypassing ACL."}
paddy@38 75 )
paddy@38 76
paddy@57 77 // Profile represents a single user of the service,
paddy@158 78 // including their authentication information.
paddy@27 79 type Profile struct {
paddy@105 80 ID uuid.ID `json:"id,omitempty"`
paddy@105 81 Name string `json:"name,omitempty"`
paddy@105 82 Passphrase string `json:"-"`
paddy@105 83 Iterations int `json:"-"`
paddy@105 84 Salt string `json:"-"`
paddy@105 85 PassphraseScheme int `json:"-"`
paddy@105 86 Compromised bool `json:"-"`
paddy@105 87 LockedUntil time.Time `json:"-"`
paddy@105 88 PassphraseReset string `json:"-"`
paddy@105 89 PassphraseResetCreated time.Time `json:"-"`
paddy@105 90 Created time.Time `json:"created,omitempty"`
paddy@105 91 LastSeen time.Time `json:"last_seen,omitempty"`
paddy@38 92 }
paddy@38 93
paddy@178 94 func (p Profile) GetModelName() string {
paddy@178 95 return "profiles"
paddy@178 96 }
paddy@178 97
paddy@178 98 func (p Profile) GetID() string {
paddy@178 99 return p.ID.String()
paddy@178 100 }
paddy@178 101
paddy@178 102 func (p Profile) GetSystem() string {
paddy@178 103 return "code.secondbit.org/auth"
paddy@178 104 }
paddy@178 105
paddy@57 106 // ApplyChange applies the properties of the passed ProfileChange
paddy@57 107 // to the Profile it is called on.
paddy@38 108 func (p *Profile) ApplyChange(change ProfileChange) {
paddy@38 109 if change.Name != nil {
paddy@38 110 p.Name = *change.Name
paddy@38 111 }
paddy@38 112 if change.Passphrase != nil {
paddy@38 113 p.Passphrase = *change.Passphrase
paddy@38 114 }
paddy@38 115 if change.Iterations != nil {
paddy@38 116 p.Iterations = *change.Iterations
paddy@38 117 }
paddy@38 118 if change.Salt != nil {
paddy@38 119 p.Salt = *change.Salt
paddy@38 120 }
paddy@38 121 if change.PassphraseScheme != nil {
paddy@38 122 p.PassphraseScheme = *change.PassphraseScheme
paddy@38 123 }
paddy@38 124 if change.Compromised != nil {
paddy@38 125 p.Compromised = *change.Compromised
paddy@38 126 }
paddy@38 127 if change.LockedUntil != nil {
paddy@38 128 p.LockedUntil = *change.LockedUntil
paddy@38 129 }
paddy@38 130 if change.PassphraseReset != nil {
paddy@38 131 p.PassphraseReset = *change.PassphraseReset
paddy@38 132 }
paddy@38 133 if change.PassphraseResetCreated != nil {
paddy@38 134 p.PassphraseResetCreated = *change.PassphraseResetCreated
paddy@38 135 }
paddy@38 136 if change.LastSeen != nil {
paddy@38 137 p.LastSeen = *change.LastSeen
paddy@38 138 }
paddy@38 139 }
paddy@38 140
paddy@57 141 // ApplyBulkChange applies the properties of the passed BulkProfileChange
paddy@57 142 // to the Profile it is called on.
paddy@44 143 func (p *Profile) ApplyBulkChange(change BulkProfileChange) {
paddy@44 144 if change.Compromised != nil {
paddy@44 145 p.Compromised = *change.Compromised
paddy@44 146 }
paddy@44 147 }
paddy@44 148
paddy@57 149 // ProfileChange represents a single atomic change to a Profile's mutable data.
paddy@38 150 type ProfileChange struct {
paddy@38 151 Name *string
paddy@38 152 Passphrase *string
paddy@69 153 Iterations *int
paddy@38 154 Salt *string
paddy@38 155 PassphraseScheme *int
paddy@38 156 Compromised *bool
paddy@38 157 LockedUntil *time.Time
paddy@38 158 PassphraseReset *string
paddy@38 159 PassphraseResetCreated *time.Time
paddy@38 160 LastSeen *time.Time
paddy@38 161 }
paddy@38 162
paddy@149 163 func (c ProfileChange) Empty() bool {
paddy@161 164 return (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@149 165 }
paddy@149 166
paddy@57 167 // Validate checks the ProfileChange it is called on
paddy@57 168 // and asserts its internal validity, or lack thereof.
paddy@57 169 // A descriptive error will be returned in the case of
paddy@57 170 // an invalid change.
paddy@38 171 func (c ProfileChange) Validate() error {
paddy@149 172 if c.Empty() {
paddy@48 173 return ErrEmptyChange
paddy@48 174 }
paddy@48 175 if c.PassphraseScheme != nil && c.Passphrase == nil {
paddy@48 176 return ErrMissingPassphrase
paddy@48 177 }
paddy@48 178 if c.PassphraseReset != nil && c.PassphraseResetCreated == nil {
paddy@48 179 return ErrMissingPassphraseResetCreated
paddy@48 180 }
paddy@48 181 if c.PassphraseReset == nil && c.PassphraseResetCreated != nil {
paddy@48 182 return ErrMissingPassphraseReset
paddy@48 183 }
paddy@48 184 if c.Salt != nil && c.Passphrase == nil {
paddy@48 185 return ErrMissingPassphrase
paddy@48 186 }
paddy@48 187 if c.Iterations != nil && c.Passphrase == nil {
paddy@48 188 return ErrMissingPassphrase
paddy@48 189 }
paddy@38 190 return nil
paddy@27 191 }
paddy@27 192
paddy@57 193 // BulkProfileChange represents a single atomic change to many Profiles' mutable data.
paddy@57 194 // It is a subset of a ProfileChange, as it doesn't make sense to mutate some of the
paddy@57 195 // ProfileChange values across many Profiles all at once.
paddy@44 196 type BulkProfileChange struct {
paddy@44 197 Compromised *bool
paddy@44 198 }
paddy@44 199
paddy@149 200 func (b BulkProfileChange) Empty() bool {
paddy@149 201 return b.Compromised == nil
paddy@149 202 }
paddy@149 203
paddy@57 204 // Validate checks the BulkProfileChange it is called on
paddy@57 205 // and asserts its internal validity, or lack thereof.
paddy@57 206 // A descriptive error will be returned in the case of an
paddy@57 207 // invalid change.
paddy@44 208 func (b BulkProfileChange) Validate() error {
paddy@149 209 if b.Empty() {
paddy@48 210 return ErrEmptyChange
paddy@48 211 }
paddy@44 212 return nil
paddy@44 213 }
paddy@44 214
paddy@57 215 // Login represents a single human-friendly identifier for
paddy@57 216 // a given Profile that can be used to log into that Profile.
paddy@57 217 // Each Profile may only have one Login for each Type.
paddy@27 218 type Login struct {
paddy@172 219 Type string `json:"type,omitempty"`
paddy@172 220 Value string `json:"value,omitempty"`
paddy@172 221 ProfileID uuid.ID `json:"profile_id,omitempty"`
paddy@172 222 Created time.Time `json:"created,omitempty"`
paddy@172 223 LastUsed time.Time `json:"last_used,omitempty"`
paddy@178 224 Verification string `json:"verification,omitempty"`
paddy@172 225 Verified bool `json:"verified"`
paddy@172 226 }
paddy@172 227
paddy@178 228 func (l Login) GetModelName() string {
paddy@178 229 return "logins"
paddy@178 230 }
paddy@178 231
paddy@178 232 func (l Login) GetID() string {
paddy@178 233 return l.Value
paddy@178 234 }
paddy@178 235
paddy@178 236 func (l Login) GetSystem() string {
paddy@178 237 return "code.secondbit.org/auth"
paddy@178 238 }
paddy@178 239
paddy@172 240 type LoginChange struct {
paddy@172 241 Verification *string `json:"verification,omitempty"`
paddy@172 242 ResendVerification *bool `json:"resend_verification,omitempty"`
paddy@27 243 }
paddy@27 244
paddy@99 245 type newProfileRequest struct {
paddy@99 246 Email string `json:"email"`
paddy@99 247 Passphrase string `json:"passphrase"`
paddy@99 248 Name string `json:"name"`
paddy@99 249 }
paddy@99 250
paddy@172 251 func validateNewProfileRequest(req *newProfileRequest) []RequestError {
paddy@172 252 errors := []RequestError{}
paddy@99 253 req.Name = strings.TrimSpace(req.Name)
paddy@99 254 req.Email = strings.TrimSpace(req.Email)
paddy@99 255 if len(req.Passphrase) < MinPassphraseLength {
paddy@172 256 errors = append(errors, RequestError{
paddy@172 257 Slug: RequestErrInsufficient,
paddy@99 258 Field: "/passphrase",
paddy@99 259 })
paddy@99 260 }
paddy@99 261 if len(req.Passphrase) > MaxPassphraseLength {
paddy@172 262 errors = append(errors, RequestError{
paddy@172 263 Slug: RequestErrOverflow,
paddy@99 264 Field: "/passphrase",
paddy@99 265 })
paddy@99 266 }
paddy@99 267 if len(req.Name) > MaxNameLength {
paddy@172 268 errors = append(errors, RequestError{
paddy@172 269 Slug: RequestErrOverflow,
paddy@99 270 Field: "/name",
paddy@99 271 })
paddy@99 272 }
paddy@99 273 if req.Email == "" {
paddy@172 274 errors = append(errors, RequestError{
paddy@172 275 Slug: RequestErrMissing,
paddy@99 276 Field: "/email",
paddy@99 277 })
paddy@99 278 }
paddy@99 279 if len(req.Email) > MaxEmailLength {
paddy@172 280 errors = append(errors, RequestError{
paddy@172 281 Slug: RequestErrOverflow,
paddy@99 282 Field: "/email",
paddy@99 283 })
paddy@99 284 }
paddy@99 285 re := regexp.MustCompile(".+@.+\\..+")
paddy@99 286 if !re.Match([]byte(req.Email)) {
paddy@172 287 errors = append(errors, RequestError{
paddy@172 288 Slug: RequestErrInvalidFormat,
paddy@99 289 Field: "/email",
paddy@99 290 })
paddy@99 291 }
paddy@99 292 return errors
paddy@99 293 }
paddy@99 294
paddy@57 295 type profileStore interface {
paddy@57 296 getProfileByID(id uuid.ID) (Profile, error)
paddy@69 297 getProfileByLogin(value string) (Profile, error)
paddy@57 298 saveProfile(profile Profile) error
paddy@57 299 updateProfile(id uuid.ID, change ProfileChange) error
paddy@57 300 updateProfiles(ids []uuid.ID, change BulkProfileChange) error
paddy@161 301 deleteProfile(id uuid.ID) error
paddy@44 302
paddy@57 303 addLogin(login Login) error
paddy@172 304 getLogin(value string) (Login, error)
paddy@69 305 removeLogin(value string, profile uuid.ID) error
paddy@160 306 removeLoginsByProfile(profile uuid.ID) error
paddy@69 307 recordLoginUse(value string, when time.Time) error
paddy@172 308 verifyLogin(value, verification string) error
paddy@57 309 listLogins(profile uuid.ID, num, offset int) ([]Login, error)
paddy@38 310 }
paddy@27 311
paddy@57 312 func (m *memstore) getProfileByID(id uuid.ID) (Profile, error) {
paddy@38 313 m.profileLock.RLock()
paddy@38 314 defer m.profileLock.RUnlock()
paddy@38 315 p, ok := m.profiles[id.String()]
paddy@38 316 if !ok {
paddy@38 317 return Profile{}, ErrProfileNotFound
paddy@38 318 }
paddy@38 319 return p, nil
paddy@27 320 }
paddy@38 321
paddy@69 322 func (m *memstore) getProfileByLogin(value string) (Profile, error) {
paddy@44 323 m.loginLock.RLock()
paddy@44 324 defer m.loginLock.RUnlock()
paddy@69 325 login, ok := m.logins[value]
paddy@44 326 if !ok {
paddy@44 327 return Profile{}, ErrLoginNotFound
paddy@44 328 }
paddy@44 329 m.profileLock.RLock()
paddy@44 330 defer m.profileLock.RUnlock()
paddy@44 331 profile, ok := m.profiles[login.ProfileID.String()]
paddy@44 332 if !ok {
paddy@44 333 return Profile{}, ErrProfileNotFound
paddy@44 334 }
paddy@44 335 return profile, nil
paddy@38 336 }
paddy@38 337
paddy@57 338 func (m *memstore) saveProfile(profile Profile) error {
paddy@38 339 m.profileLock.Lock()
paddy@38 340 defer m.profileLock.Unlock()
paddy@38 341 _, ok := m.profiles[profile.ID.String()]
paddy@38 342 if ok {
paddy@38 343 return ErrProfileAlreadyExists
paddy@38 344 }
paddy@38 345 m.profiles[profile.ID.String()] = profile
paddy@38 346 return nil
paddy@38 347 }
paddy@38 348
paddy@57 349 func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error {
paddy@38 350 m.profileLock.Lock()
paddy@38 351 defer m.profileLock.Unlock()
paddy@38 352 p, ok := m.profiles[id.String()]
paddy@38 353 if !ok {
paddy@38 354 return ErrProfileNotFound
paddy@38 355 }
paddy@38 356 p.ApplyChange(change)
paddy@38 357 m.profiles[id.String()] = p
paddy@38 358 return nil
paddy@38 359 }
paddy@38 360
paddy@57 361 func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error {
paddy@44 362 m.profileLock.Lock()
paddy@44 363 defer m.profileLock.Unlock()
paddy@44 364 for id, profile := range m.profiles {
paddy@44 365 for _, i := range ids {
paddy@44 366 if id == i.String() {
paddy@44 367 profile.ApplyBulkChange(change)
paddy@44 368 m.profiles[id] = profile
paddy@44 369 break
paddy@44 370 }
paddy@44 371 }
paddy@44 372 }
paddy@44 373 return nil
paddy@44 374 }
paddy@44 375
paddy@161 376 func (m *memstore) deleteProfile(id uuid.ID) error {
paddy@161 377 m.profileLock.Lock()
paddy@161 378 defer m.profileLock.Unlock()
paddy@161 379 if _, ok := m.profiles[id.String()]; !ok {
paddy@161 380 return ErrProfileNotFound
paddy@161 381 }
paddy@161 382 delete(m.profiles, id.String())
paddy@161 383 return nil
paddy@161 384 }
paddy@161 385
paddy@57 386 func (m *memstore) addLogin(login Login) error {
paddy@44 387 m.loginLock.Lock()
paddy@44 388 defer m.loginLock.Unlock()
paddy@69 389 _, ok := m.logins[login.Value]
paddy@44 390 if ok {
paddy@44 391 return ErrLoginAlreadyExists
paddy@44 392 }
paddy@69 393 m.logins[login.Value] = login
paddy@69 394 m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Value)
paddy@44 395 return nil
paddy@44 396 }
paddy@44 397
paddy@172 398 func (m *memstore) getLogin(value string) (Login, error) {
paddy@172 399 m.loginLock.RLock()
paddy@172 400 defer m.loginLock.RUnlock()
paddy@172 401 l, ok := m.logins[value]
paddy@172 402 if !ok {
paddy@172 403 return Login{}, ErrLoginNotFound
paddy@172 404 }
paddy@172 405 return l, nil
paddy@172 406 }
paddy@172 407
paddy@69 408 func (m *memstore) removeLogin(value string, profile uuid.ID) error {
paddy@44 409 m.loginLock.Lock()
paddy@44 410 defer m.loginLock.Unlock()
paddy@69 411 l, ok := m.logins[value]
paddy@44 412 if !ok {
paddy@44 413 return ErrLoginNotFound
paddy@44 414 }
paddy@44 415 if !l.ProfileID.Equal(profile) {
paddy@44 416 return ErrLoginNotFound
paddy@44 417 }
paddy@69 418 delete(m.logins, value)
paddy@44 419 pos := -1
paddy@44 420 for p, id := range m.profileLoginLookup[profile.String()] {
paddy@69 421 if id == value {
paddy@44 422 pos = p
paddy@44 423 break
paddy@44 424 }
paddy@44 425 }
paddy@44 426 if pos >= 0 {
paddy@44 427 m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...)
paddy@44 428 }
paddy@44 429 return nil
paddy@44 430 }
paddy@44 431
paddy@160 432 func (m *memstore) removeLoginsByProfile(profile uuid.ID) error {
paddy@160 433 m.loginLock.Lock()
paddy@160 434 defer m.loginLock.Unlock()
paddy@160 435 logins, ok := m.profileLoginLookup[profile.String()]
paddy@160 436 if !ok {
paddy@160 437 return ErrProfileNotFound
paddy@160 438 }
paddy@160 439 delete(m.profileLoginLookup, profile.String())
paddy@160 440 for _, login := range logins {
paddy@160 441 delete(m.logins, login)
paddy@160 442 }
paddy@160 443 return nil
paddy@160 444 }
paddy@160 445
paddy@69 446 func (m *memstore) recordLoginUse(value string, when time.Time) error {
paddy@44 447 m.loginLock.Lock()
paddy@44 448 defer m.loginLock.Unlock()
paddy@69 449 l, ok := m.logins[value]
paddy@44 450 if !ok {
paddy@44 451 return ErrLoginNotFound
paddy@44 452 }
paddy@44 453 l.LastUsed = when
paddy@69 454 m.logins[value] = l
paddy@44 455 return nil
paddy@44 456 }
paddy@44 457
paddy@172 458 func (m *memstore) verifyLogin(value, verification string) error {
paddy@172 459 m.loginLock.Lock()
paddy@172 460 defer m.loginLock.Unlock()
paddy@172 461 l, ok := m.logins[value]
paddy@172 462 if !ok {
paddy@172 463 return ErrLoginNotFound
paddy@172 464 }
paddy@172 465 if l.Verification != verification {
paddy@172 466 return ErrLoginVerificationInvalid
paddy@172 467 }
paddy@172 468 l.Verified = true
paddy@172 469 m.logins[value] = l
paddy@172 470 return nil
paddy@172 471 }
paddy@172 472
paddy@57 473 func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) {
paddy@44 474 m.loginLock.RLock()
paddy@44 475 defer m.loginLock.RUnlock()
paddy@44 476 ids, ok := m.profileLoginLookup[profile.String()]
paddy@44 477 if !ok {
paddy@44 478 return []Login{}, nil
paddy@44 479 }
paddy@44 480 if len(ids) > num+offset {
paddy@44 481 ids = ids[offset : num+offset]
paddy@44 482 } else if len(ids) > offset {
paddy@44 483 ids = ids[offset:]
paddy@44 484 } else {
paddy@44 485 return []Login{}, nil
paddy@44 486 }
paddy@44 487 logins := []Login{}
paddy@44 488 for _, id := range ids {
paddy@44 489 login, ok := m.logins[id]
paddy@44 490 if !ok {
paddy@44 491 continue
paddy@44 492 }
paddy@44 493 logins = append(logins, login)
paddy@44 494 }
paddy@44 495 return logins, nil
paddy@44 496 }
paddy@99 497
paddy@160 498 func cleanUpAfterProfileDeletion(profile uuid.ID, context Context) {
paddy@160 499 err := context.RemoveLoginsByProfile(profile)
paddy@160 500 if err != nil {
paddy@160 501 log.Printf("Error removing logins from profile %s: %+v\n", profile, err)
paddy@160 502 }
paddy@162 503 err = context.TerminateSessionsByProfile(profile)
paddy@162 504 if err != nil {
paddy@162 505 log.Printf("Error terminating sessions associated with profile %s: %+v\n", profile, err)
paddy@162 506 }
paddy@162 507 err = context.RevokeTokensByProfileID(profile)
paddy@162 508 if err != nil {
paddy@162 509 log.Printf("Error revoking tokens associated with profile %s: %+v\n", profile, err)
paddy@162 510 }
paddy@163 511 err = context.DeleteAuthorizationCodesByProfileID(profile)
paddy@163 512 if err != nil {
paddy@163 513 log.Printf("Error deleting authorization codes associated with profile %s: %+v\n", profile, err)
paddy@163 514 }
paddy@164 515 clients, err := context.ListClientsByOwner(profile, -1, 0)
paddy@164 516 if err != nil {
paddy@164 517 log.Printf("Error listing clients by profile %s: %+v\n", profile, err)
paddy@164 518 }
paddy@164 519 err = context.DeleteClientsByOwner(profile)
paddy@164 520 if err != nil {
paddy@164 521 log.Printf("Error deleting clients by profile %s: %+v\n", profile, err)
paddy@164 522 }
paddy@164 523 for _, client := range clients {
paddy@164 524 cleanUpAfterClientDeletion(client.ID, context)
paddy@164 525 }
paddy@160 526 }
paddy@160 527
paddy@105 528 // RegisterProfileHandlers adds handlers to the passed router to handle the profile endpoints, like registration and user retrieval.
paddy@105 529 func RegisterProfileHandlers(r *mux.Router, context Context) {
paddy@165 530 r.Handle("/profiles", wrap(context, CreateProfileHandler)).Methods("POST", "OPTIONS")
paddy@166 531 r.Handle("/profiles/{id}", wrap(context, GetProfileHandler)).Methods("GET", "OPTIONS")
paddy@165 532 r.Handle("/profiles/{id}", wrap(context, UpdateProfileHandler)).Methods("PATCH", "OPTIONS")
paddy@165 533 r.Handle("/profiles/{id}", wrap(context, DeleteProfileHandler)).Methods("DELETE", "OPTIONS")
paddy@128 534 // BUG(paddy): We need to implement a handler that will add a login to a profile.
paddy@128 535 // BUG(paddy): We need to implement a handler that will remove a login from a profile. What happens to sessions created with that login?
paddy@128 536 // BUG(paddy): We need to implement a handler that will list the logins attached to a profile.
paddy@172 537 r.Handle("/logins/{login}", wrap(context, GetLoginHandler)).Methods("GET", "OPTIONS")
paddy@172 538 r.Handle("/logins/{login}", wrap(context, UpdateLoginHandler)).Methods("PUT", "PATCH", "OPTIONS")
paddy@105 539 }
paddy@105 540
paddy@166 541 // GetProfileHandler is an HTTP handler for retrieving a profile.
paddy@166 542 func GetProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@172 543 errors := []RequestError{}
paddy@166 544 authz := r.Header.Get("Authorization")
paddy@166 545 if !strings.HasPrefix(authz, "Bearer ") {
paddy@172 546 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 547 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
paddy@166 548 return
paddy@166 549 }
paddy@166 550 authz = strings.TrimPrefix(authz, "Bearer ")
paddy@166 551 vars := mux.Vars(r)
paddy@166 552 if vars["id"] == "" {
paddy@172 553 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
paddy@172 554 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@166 555 return
paddy@166 556 }
paddy@166 557 id, err := uuid.Parse(vars["id"])
paddy@166 558 if err != nil {
paddy@172 559 errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"})
paddy@172 560 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@166 561 return
paddy@166 562 }
paddy@166 563 token, err := context.GetToken(authz, false)
paddy@166 564 if err != nil || token.Revoked {
paddy@166 565 if err == ErrTokenNotFound || token.Revoked {
paddy@172 566 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 567 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
paddy@166 568 return
paddy@166 569 } else {
paddy@166 570 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@166 571 return
paddy@166 572 }
paddy@166 573 }
paddy@166 574 if !id.Equal(token.ProfileID) {
paddy@172 575 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 576 encode(w, r, http.StatusForbidden, Response{Errors: errors})
paddy@166 577 return
paddy@166 578 }
paddy@166 579 profile, err := context.GetProfileByID(id)
paddy@166 580 if err != nil {
paddy@166 581 if err == ErrProfileNotFound {
paddy@172 582 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
paddy@172 583 encode(w, r, http.StatusNotFound, Response{Errors: errors})
paddy@166 584 return
paddy@166 585 }
paddy@166 586 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@166 587 return
paddy@166 588 }
paddy@172 589 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
paddy@166 590 return
paddy@166 591 }
paddy@166 592
paddy@99 593 // CreateProfileHandler is an HTTP handler for registering new profiles.
paddy@99 594 func CreateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@99 595 scheme, ok := passphraseSchemes[CurPassphraseScheme]
paddy@99 596 if !ok {
paddy@149 597 log.Printf("Error selecting passphrase scheme #%d\n", CurPassphraseScheme)
paddy@105 598 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 599 return
paddy@99 600 }
paddy@99 601 var req newProfileRequest
paddy@172 602 errors := []RequestError{}
paddy@99 603 decoder := json.NewDecoder(r.Body)
paddy@99 604 err := decoder.Decode(&req)
paddy@99 605 if err != nil {
paddy@105 606 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
paddy@99 607 return
paddy@99 608 }
paddy@99 609 errors = append(errors, validateNewProfileRequest(&req)...)
paddy@99 610 if len(errors) > 0 {
paddy@172 611 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@99 612 return
paddy@99 613 }
paddy@99 614 passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations)
paddy@99 615 if err != nil {
paddy@149 616 log.Printf("Error creating encoded passphrase: %#+v\n", err)
paddy@105 617 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 618 return
paddy@99 619 }
paddy@99 620 profile := Profile{
paddy@99 621 ID: uuid.NewID(),
paddy@99 622 Name: req.Name,
paddy@99 623 Passphrase: string(passphrase),
paddy@99 624 Iterations: context.config.iterations,
paddy@99 625 Salt: string(salt),
paddy@99 626 PassphraseScheme: CurPassphraseScheme,
paddy@99 627 Created: time.Now(),
paddy@99 628 LastSeen: time.Now(),
paddy@99 629 }
paddy@99 630 err = context.SaveProfile(profile)
paddy@99 631 if err != nil {
paddy@105 632 if err == ErrProfileAlreadyExists {
paddy@172 633 encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/id"}}})
paddy@105 634 return
paddy@105 635 }
paddy@149 636 log.Printf("Error saving profile: %#+v\n", err)
paddy@105 637 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 638 return
paddy@99 639 }
paddy@99 640 logins := []Login{}
paddy@99 641 login := Login{
paddy@172 642 Type: "email",
paddy@172 643 Value: req.Email,
paddy@172 644 Created: profile.Created,
paddy@172 645 LastUsed: profile.Created,
paddy@172 646 ProfileID: profile.ID,
paddy@172 647 Verification: uuid.NewID().String(),
paddy@99 648 }
paddy@99 649 err = context.AddLogin(login)
paddy@99 650 if err != nil {
paddy@105 651 if err == ErrLoginAlreadyExists {
paddy@172 652 encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/email"}}})
paddy@105 653 return
paddy@105 654 }
paddy@149 655 log.Printf("Error adding login: %#+v\n", err)
paddy@105 656 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@99 657 return
paddy@99 658 }
paddy@178 659 verification := login.Verification
paddy@178 660 login.Verification = "" // clear verification so it's not exposed
paddy@99 661 logins = append(logins, login)
paddy@172 662 resp := Response{
paddy@105 663 Logins: logins,
paddy@105 664 Profiles: []Profile{profile},
paddy@105 665 }
paddy@105 666 encode(w, r, http.StatusCreated, resp)
paddy@178 667 login.Verification = verification // restore verification so it's included in the event
paddy@178 668 go context.SendModelEvent(login, events.ActionCreated)
paddy@178 669 go context.SendModelEvent(profile, events.ActionCreated)
paddy@99 670 }
paddy@145 671
paddy@145 672 func UpdateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@172 673 errors := []RequestError{}
paddy@145 674 vars := mux.Vars(r)
paddy@145 675 if vars["id"] == "" {
paddy@172 676 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
paddy@172 677 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@145 678 return
paddy@145 679 }
paddy@145 680 id, err := uuid.Parse(vars["id"])
paddy@145 681 if err != nil {
paddy@172 682 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 683 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@145 684 return
paddy@145 685 }
paddy@145 686 username, password, ok := r.BasicAuth()
paddy@145 687 if !ok {
paddy@172 688 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 689 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
paddy@145 690 return
paddy@145 691 }
paddy@145 692 profile, err := authenticate(username, password, context)
paddy@145 693 if err != nil {
paddy@145 694 if isAuthError(err) {
paddy@172 695 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 696 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
paddy@145 697 } else {
paddy@172 698 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
paddy@172 699 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
paddy@145 700 }
paddy@145 701 return
paddy@145 702 }
paddy@145 703 if !profile.ID.Equal(id) {
paddy@172 704 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 705 encode(w, r, http.StatusForbidden, Response{Errors: errors})
paddy@145 706 return
paddy@145 707 }
paddy@145 708 var req ProfileChange
paddy@145 709 decoder := json.NewDecoder(r.Body)
paddy@145 710 err = decoder.Decode(&req)
paddy@145 711 if err != nil {
paddy@149 712 log.Printf("Error decoding request: %#+v\n", err)
paddy@145 713 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
paddy@145 714 return
paddy@145 715 }
paddy@145 716 req.Iterations = nil
paddy@145 717 req.Salt = nil
paddy@145 718 req.PassphraseScheme = nil
paddy@145 719 req.Compromised = nil // BUG(paddy): Need a way for admins to mark accounts as compromised
paddy@145 720 req.LockedUntil = nil
paddy@145 721 req.LastSeen = nil
paddy@145 722 if req.Passphrase != nil {
paddy@145 723 if len(*req.Passphrase) < MinPassphraseLength {
paddy@172 724 errors = append(errors, RequestError{Slug: RequestErrInsufficient, Field: "/passphrase"})
paddy@172 725 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@145 726 return
paddy@145 727 }
paddy@145 728 if len(*req.Passphrase) > MaxPassphraseLength {
paddy@172 729 errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/passphrase"})
paddy@172 730 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@145 731 return
paddy@145 732 }
paddy@145 733 iterations := context.config.iterations
paddy@145 734 scheme, ok := passphraseSchemes[CurPassphraseScheme]
paddy@145 735 if !ok {
paddy@145 736 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@145 737 return
paddy@145 738 }
paddy@145 739 curScheme := CurPassphraseScheme
paddy@145 740 req.PassphraseScheme = &curScheme
paddy@145 741 passphrase, salt, err := scheme.create(*req.Passphrase, iterations)
paddy@145 742 if err != nil {
paddy@145 743 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@145 744 return
paddy@145 745 }
paddy@145 746 req.Passphrase = &passphrase
paddy@145 747 req.Salt = &salt
paddy@145 748 req.Iterations = &iterations
paddy@145 749 }
paddy@145 750 if req.PassphraseReset != nil {
paddy@145 751 now := time.Now()
paddy@145 752 req.PassphraseResetCreated = &now
paddy@145 753 }
paddy@145 754 err = req.Validate()
paddy@145 755 if err != nil {
paddy@145 756 var status int
paddy@172 757 var resp Response
paddy@145 758 switch err {
paddy@145 759 case ErrEmptyChange:
paddy@145 760 resp.Profiles = []Profile{profile}
paddy@145 761 status = http.StatusOK
paddy@145 762 default:
paddy@172 763 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
paddy@145 764 resp.Errors = errors
paddy@145 765 status = http.StatusInternalServerError
paddy@145 766 }
paddy@145 767 encode(w, r, status, resp)
paddy@145 768 return
paddy@145 769 }
paddy@145 770 err = context.UpdateProfile(id, req)
paddy@145 771 if err != nil {
paddy@145 772 if err == ErrProfileNotFound {
paddy@172 773 errors = append(errors, RequestError{Slug: RequestErrNotFound})
paddy@172 774 encode(w, r, http.StatusNotFound, Response{Errors: errors})
paddy@145 775 return
paddy@145 776 }
paddy@145 777 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@145 778 return
paddy@145 779 }
paddy@145 780 profile.ApplyChange(req)
paddy@172 781 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
paddy@145 782 return
paddy@145 783 }
paddy@160 784
paddy@160 785 func DeleteProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@172 786 errors := []RequestError{}
paddy@160 787 vars := mux.Vars(r)
paddy@160 788 if vars["id"] == "" {
paddy@172 789 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
paddy@172 790 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@160 791 return
paddy@160 792 }
paddy@160 793 id, err := uuid.Parse(vars["id"])
paddy@160 794 if err != nil {
paddy@172 795 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 796 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@160 797 return
paddy@160 798 }
paddy@160 799 username, password, ok := r.BasicAuth()
paddy@160 800 if !ok {
paddy@172 801 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 802 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
paddy@160 803 return
paddy@160 804 }
paddy@160 805 profile, err := authenticate(username, password, context)
paddy@160 806 if err != nil {
paddy@160 807 if isAuthError(err) {
paddy@172 808 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 809 encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
paddy@160 810 } else {
paddy@172 811 errors = append(errors, RequestError{Slug: RequestErrActOfGod})
paddy@172 812 encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
paddy@160 813 }
paddy@160 814 return
paddy@160 815 }
paddy@160 816 if !profile.ID.Equal(id) {
paddy@172 817 errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
paddy@172 818 encode(w, r, http.StatusForbidden, Response{Errors: errors})
paddy@160 819 return
paddy@160 820 }
paddy@161 821 err = context.DeleteProfile(id)
paddy@160 822 if err != nil {
paddy@160 823 if err == ErrProfileNotFound {
paddy@172 824 errors = append(errors, RequestError{Slug: RequestErrNotFound})
paddy@172 825 encode(w, r, http.StatusNotFound, Response{Errors: errors})
paddy@160 826 return
paddy@160 827 }
paddy@160 828 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@160 829 return
paddy@160 830 }
paddy@172 831 encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
paddy@160 832 go cleanUpAfterProfileDeletion(profile.ID, context)
paddy@160 833 }
paddy@172 834
paddy@172 835 func GetLoginHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@172 836 var errors []RequestError
paddy@172 837 vars := mux.Vars(r)
paddy@172 838 if vars["login"] == "" {
paddy@172 839 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"})
paddy@172 840 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@172 841 return
paddy@172 842 }
paddy@172 843 login, err := context.GetLogin(vars["login"])
paddy@172 844 if err != nil {
paddy@172 845 if err == ErrLoginNotFound {
paddy@172 846 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
paddy@172 847 encode(w, r, http.StatusNotFound, Response{Errors: errors})
paddy@172 848 return
paddy@172 849 }
paddy@172 850 log.Printf("Error retrieving login: %#+v\n", err)
paddy@172 851 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@172 852 return
paddy@172 853 }
paddy@178 854 // clear verification code so it's not exposed
paddy@178 855 // BUG(paddy): We hsould only hide the verification code if it's not an admin request, but auth isn't set up properly for scopes yet
paddy@178 856 login.Verification = ""
paddy@172 857 encode(w, r, http.StatusOK, Response{Logins: []Login{login}})
paddy@172 858 }
paddy@172 859
paddy@172 860 func UpdateLoginHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@172 861 var errors []RequestError
paddy@172 862 vars := mux.Vars(r)
paddy@172 863 if vars["login"] == "" {
paddy@172 864 errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"})
paddy@172 865 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@172 866 return
paddy@172 867 }
paddy@172 868 var req LoginChange
paddy@172 869 decoder := json.NewDecoder(r.Body)
paddy@172 870 err := decoder.Decode(&req)
paddy@172 871 if err != nil {
paddy@172 872 log.Printf("Error decoding request: %#+v\n", err)
paddy@172 873 encode(w, r, http.StatusBadRequest, invalidFormatResponse)
paddy@172 874 return
paddy@172 875 }
paddy@172 876 login, err := context.GetLogin(vars["login"])
paddy@172 877 if err != nil {
paddy@172 878 if err == ErrLoginNotFound {
paddy@172 879 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
paddy@172 880 encode(w, r, http.StatusNotFound, Response{Errors: errors})
paddy@172 881 return
paddy@172 882 }
paddy@172 883 log.Printf("Error retrieving login: %#+v\n", err)
paddy@172 884 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@172 885 return
paddy@172 886 }
paddy@172 887 if req.Verification != nil {
paddy@172 888 err = context.VerifyLogin(vars["login"], *req.Verification)
paddy@172 889 if err != nil {
paddy@172 890 if err == ErrLoginNotFound {
paddy@172 891 errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
paddy@172 892 encode(w, r, http.StatusNotFound, Response{Errors: errors})
paddy@172 893 return
paddy@172 894 } else if err == ErrLoginVerificationInvalid {
paddy@172 895 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/verification"})
paddy@172 896 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@172 897 return
paddy@172 898 }
paddy@172 899 log.Printf("Error verifying login with verification '%s': %#+v\n", *req.Verification, err)
paddy@172 900 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@172 901 return
paddy@172 902 }
paddy@179 903 go context.SendModelEvent(login, ActionLoginVerified)
paddy@172 904 login.Verified = true
paddy@172 905 } else if req.ResendVerification != nil {
paddy@172 906 if !*req.ResendVerification {
paddy@172 907 errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/resend_verification"})
paddy@172 908 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@172 909 return
paddy@172 910 }
paddy@178 911 go context.SendModelEvent(login, ActionResendVerification)
paddy@172 912 } else {
paddy@172 913 errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/"})
paddy@172 914 encode(w, r, http.StatusBadRequest, Response{Errors: errors})
paddy@172 915 return
paddy@172 916 }
paddy@178 917 // clear the Verification code so it's not exposed
paddy@178 918 login.Verification = ""
paddy@172 919 encode(w, r, http.StatusOK, Response{Logins: []Login{login}})
paddy@172 920 }