auth

Paddy 2015-07-18 Parent:0a2c3d677161 Child:b7e685839a1b

179:7bba108d2d9a Go to Latest

auth/profile.go

Send events when logins are verified. Add an ActionLoginVerified constant to use as the action when a login has been verified. On second thought, this should probably just be "verified", huh? Then we can reuse it across models. Oops. We also added a call to send a login verified event to NSQ when the login is verified.

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 }