Skip to content

Commit

Permalink
Password reset rate limiting & existing token invalidation
Browse files Browse the repository at this point in the history
  • Loading branch information
darh committed Feb 24, 2022
1 parent 6be0242 commit d2d0245
Show file tree
Hide file tree
Showing 4 changed files with 301 additions and 51 deletions.
201 changes: 153 additions & 48 deletions system/service/auth.go
Expand Up @@ -64,10 +64,17 @@ const (

passwordMinLength = 8
passwordMaxLength = 256

tokenReqMaxCount = 5
tokenReqMaxWindow = time.Minute * 15
)

var (
reEmail = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")

oneTokenPerUser = map[string]bool{
credentialsTypeResetPasswordToken: true,
}
)

func defaultProviderValidator(provider string) error {
Expand Down Expand Up @@ -1007,67 +1014,55 @@ func (svc auth) procLogin(ctx context.Context, s store.Storer, u *types.User, c
return nil
}

func (svc auth) loadUserFromToken(ctx context.Context, token, kind string) (u *types.User, err error) {
// Loads user from token and removes that token right after
func (svc auth) loadUserFromToken(ctx context.Context, token, kind string) (u *types.User, _ error) {
var (
aam = &authActionProps{
credentials: &types.Credentials{Kind: kind},
}
)

credentialsID, credentials := svc.validateToken(token)
if credentialsID == 0 {
return nil, AuthErrInvalidToken(aam)
}

c, err := store.LookupCredentialsByID(ctx, svc.store, credentialsID)
if errors.IsNotFound(err) {
return nil, AuthErrInvalidToken(aam)
}

aam.setCredentials(c)

if err != nil {
return
}
return u, svc.store.Tx(ctx, func(ctx context.Context, s store.Storer) (err error) {
credentialsID, credentials := validateToken(token)
if credentialsID == 0 {
return AuthErrInvalidToken(aam)
}

if err = store.DeleteCredentialsByID(ctx, svc.store, c.ID); err != nil {
return
}
c, err := store.LookupCredentialsByID(ctx, s, credentialsID)
if errors.IsNotFound(err) {
return AuthErrInvalidToken(aam)
}

if !c.Valid() || c.Credentials != credentials {
return nil, AuthErrInvalidToken(aam)
}
aam.setCredentials(c)

u, err = store.LookupUserByID(ctx, svc.store, c.OwnerID)
if err != nil {
return nil, err
}
if err != nil {
return
}

aam.setUser(u)
if err = store.DeleteCredentialsByID(ctx, s, c.ID); err != nil {
return
}

// context will be updated with new identity
// in the caller fn
if !c.Valid() || c.Credentials != credentials {
return AuthErrInvalidToken(aam)
}

if !u.Valid() {
return nil, AuthErrInvalidCredentials(aam)
}
u, err = store.LookupUserByID(ctx, s, c.OwnerID)
if err != nil {
return err
}

return u, nil
}
aam.setUser(u)

func (svc auth) validateToken(token string) (ID uint64, credentials string) {
// Token = <32 random chars><credentials-id>
if len(token) <= credentialsTokenLength {
return
}
// context will be updated with new identity
// in the caller fn

ID, _ = strconv.ParseUint(token[credentialsTokenLength:], 10, 64)
if ID == 0 {
return
}
if !u.Valid() {
return AuthErrInvalidCredentials(aam)
}

credentials = token[:credentialsTokenLength]
return
return nil
})
}

// Generates & stores user token
Expand All @@ -1081,7 +1076,35 @@ func (svc auth) createUserToken(ctx context.Context, u *types.User, kind string)
}
)

err = func() error {
err = svc.store.Tx(ctx, func(ctx context.Context, s store.Storer) (err error) {
if u == nil || u.ID == 0 {
return AuthErrGeneric()
}

// Rate limit requests
cc, _, err := store.SearchCredentials(ctx, s, types.CredentialsFilter{
OwnerID: u.ID,
Kind: kind,

// we want to count deleted tokens as well
Deleted: filter.StateInclusive,
})

if err != nil {
return err
}

// gt/eq since this current request is not yet stored
if err = svc.checkTokenRate(cc, tokenReqMaxWindow, tokenReqMaxCount); err != nil {
return
}

// removes expired and soft-deleted tokens
// and enforces one-token-per-user rule
if err = svc.cleanupCredentials(ctx, s, cc); err != nil {
return
}

switch kind {
case credentialsTypeMFAEmailOTP:
expSec := svc.settings.Auth.MultiFactor.EmailOTP.Expires
Expand Down Expand Up @@ -1118,7 +1141,7 @@ func (svc auth) createUserToken(ctx context.Context, u *types.User, kind string)
ExpiresAt: &expiresAt,
}

err = store.CreateCredentials(ctx, svc.store, c)
err = store.CreateCredentials(ctx, s, c)

if err != nil {
return err
Expand All @@ -1134,11 +1157,78 @@ func (svc auth) createUserToken(ctx context.Context, u *types.User, kind string)
}

return nil
}()
})

return token, svc.recordAction(ctx, aam, AuthActionIssueToken, err)
}

// checks existing tokens and ensure that the creation rate is within limits
func (svc auth) checkTokenRate(cc types.CredentialsSet, window time.Duration, max int) error {
if len(cc) == 0 || window == 0 || max == 0 {
return nil
}

var (
cutoff = now().Add(window * -1)
count = 0
)

for _, c := range cc {
if c.CreatedAt.Before(cutoff) {
// skip tokens created before cutoff
continue
}

count++

if count > max {
break
}
}

if count > max {
return AuthErrRateLimitExceeded()
}

return nil
}

func (svc auth) cleanupCredentials(ctx context.Context, s store.Credentials, cc types.CredentialsSet) (err error) {
var (
update types.CredentialsSet
remove types.CredentialsSet
)

for _, c := range cc {
switch {
case oneTokenPerUser[c.Kind]:
// if token type is shortlisted in one-token-per-user
// mark all existing tokens as deleted if to
//
// only want to mark them as deleted ad
c.DeletedAt = now()
update = append(update, c)

case false, // just a placeholder
c.DeletedAt.Add(tokenReqMaxWindow).Before(*now()),
c.ExpiresAt.Before(*now()):
// schedule all soft-deleted and expired token
// for removal
remove = append(remove, c)
}
}

if err = store.UpdateCredentials(ctx, s, update...); err != nil {
return
}

if err = store.DeleteCredentials(ctx, s, remove...); err != nil {
return
}

return
}

// Automatically promotes user to super-administrator if it is the first non-system user in the database
func (svc auth) autoPromote(ctx context.Context, u *types.User) (err error) {
var (
Expand Down Expand Up @@ -1539,3 +1629,18 @@ func (svc auth) RemoveAccessTokens(ctx context.Context, user *types.User) error
svc.store.DeleteAuthOA2TokenByUserID(ctx, user.ID),
)
}

func validateToken(token string) (ID uint64, credentials string) {
// Token = <32 random chars><credentials-id>
if len(token) <= credentialsTokenLength {
return
}

ID, _ = strconv.ParseUint(token[credentialsTokenLength:], 10, 64)
if ID == 0 {
return
}

credentials = token[:credentialsTokenLength]
return
}
36 changes: 36 additions & 0 deletions system/service/auth_actions.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions system/service/auth_actions.yaml
Expand Up @@ -203,6 +203,10 @@ errors:
message: "invalid code"
severity: warning

- error: rateLimitExceeded
message: "rate limit exceeded"
log: "rate limit exceeded for {{user}}"

- error: maxUserLimitReached
message: "you have reached your user limit, contact your Corteza administrator"
severity: warning

0 comments on commit d2d0245

Please sign in to comment.