auth

Paddy 2015-05-17 Parent:581c60f8dd23 Child:b0d1b3e39fc8

172:8ecb60d29b0d Go to Latest

auth/profile.go

Support email verification. The bulk of this commit is auto-modifying files to export variables (mostly our request error types and our response type) so that they can be reused in a Go client for that API. We also implement the beginnings of a Go client for that API, implementing the bare minimum we need for our immediate purposes: the ability to retrieve information about a Login. This, of course, means we need an API endpoint that will return information about a Login, which in turn required us to implement a GetLogin method in our profileStore. Which got in-memory and postgres implementations. That done, we could add the Verification field and Verified field to the Login type, to keep track of whether we've verified the user's ownership of those communication methods (if the Login is, in fact, a communication method). This required us to update sql/postgres_init.sql to account for the new fields we're tracking. It also means that when creating a Login, we had to generate a UUID to use as the Verification field. To make things complete, we needed a verifyLogin method on the profileStore to mark a Login as verified. That, in turn, required an endpoint to control this through the API. While doing so, I lumped things together in an UpdateLogin handler just so we could reuse the endpoint and logic when resending a verification email that may have never reached the user, for whatever reason (the quintessential "send again" button). Finally, we implemented an email_verification listener that will pull email_verification events off NSQ, check for the requisite data integrity, and use mailgun to email out a verification/welcome email.

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