auth

Paddy 2015-05-15 Parent:0ff23f3a4ede Child:8ecb60d29b0d

168:581c60f8dd23 Go to Latest

auth/profile.go

Switch to a JWT approach. We're going to use a JWT as our access tokens (as discussed in &yet's excellent post https://blog.andyet.com/2015/05/12/micro-services-user-info-and-auth and my ensuing conversation with Fritzy). The benefit of this approach is that we can do authentication and even some authorization without touching the database at all. The drawback is that we can no longer revoke access tokens, only the refresh tokens that grant the access tokens. We need a new config variable to set our private key, used to sign the JWT. We get to remove our token handlers, as we no longer can revoke tokens, so there's no purpose in getting information about it or listing them. Our tokenStore revokeToken gets to be simplified, as it will only ever be used for refresh tokens now. We also updated our postgres and memstore implementations. We added a helper method for generating the signed "access token" (our JWT) and started using it in the places where we're creating a Token. We get to remove the `revoked` SQL column for the tokens table, and rename the `refresh_revoked` column to just be `revoked`. We shortened our access token expiration to 15 minutes instead of an hour, to deal with the token not being revokable.

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