Merge pull request #428 from gravitational/ev/lock

Account lock after N unsuccessful login attempts
This commit is contained in:
Alexander Klizhentas 2016-05-30 20:31:49 -07:00
commit 032e743ca6
6 changed files with 82 additions and 20 deletions

View file

@ -96,7 +96,7 @@ func (s *APISuite) SetUpTest(c *C) {
s.LockS = local.NewLockService(s.bk)
s.PresenceS = local.NewPresenceService(s.bk)
s.ProvisioningS = local.NewProvisioningService(s.bk)
s.WebS = local.NewIdentityService(s.bk)
s.WebS = local.NewIdentityService(s.bk, 10, time.Duration(time.Hour))
}
func (s *APISuite) TearDownTest(c *C) {

View file

@ -96,7 +96,8 @@ func NewAuthServer(cfg *InitConfig, opts ...AuthServerOption) *AuthServer {
cfg.Provisioner = local.NewProvisioningService(cfg.Backend)
}
if cfg.Identity == nil {
cfg.Identity = local.NewIdentityService(cfg.Backend)
cfg.Identity = local.NewIdentityService(cfg.Backend,
defaults.MaxLoginAttempts, defaults.AccountLockInterval)
}
as := AuthServer{
bk: cfg.Backend,

View file

@ -122,6 +122,14 @@ const (
// SessionLingerTTL defines for how long abandoned sessions remain active,
// waiting for their parties to restore connection (before being garbage-collected)
SessionLingerTTL = time.Duration(time.Second * 5)
// MaxLoginAttempts sets the max. number of allowed failed login attempts
// before a user account is locked for AccountLockInterval
MaxLoginAttempts byte = 5
// AccountLockInterval defines a time interval during which a user account
// is locked after MaxLoginAttempts
AccountLockInterval = time.Duration(20 * time.Minute)
)
var (

View file

@ -19,6 +19,7 @@ package local
import (
"path/filepath"
"testing"
"time"
"github.com/gravitational/teleport/lib/backend/boltbk"
"github.com/gravitational/teleport/lib/services/suite"
@ -53,7 +54,7 @@ func (s *BoltSuite) SetUpTest(c *C) {
suite.LockS = NewLockService(s.bk)
suite.PresenceS = NewPresenceService(s.bk)
suite.ProvisioningS = NewProvisioningService(s.bk)
suite.WebS = NewIdentityService(s.bk)
suite.WebS = NewIdentityService(s.bk, 10, time.Duration(time.Hour))
suite.ChangesC = make(chan interface{})
s.suite = suite
}

View file

@ -35,13 +35,21 @@ import (
// IdentityService is responsible for managing web users and currently
// user accounts as well
type IdentityService struct {
backend backend.Backend
backend backend.Backend
lockAfter byte
lockDuration time.Duration
}
// NewIdentityService returns new instance of WebService
func NewIdentityService(backend backend.Backend) *IdentityService {
// NewIdentityService returns a new instance of IdentityService object
func NewIdentityService(
backend backend.Backend,
lockAfter byte,
lockDuration time.Duration) *IdentityService {
return &IdentityService{
backend: backend,
backend: backend,
lockAfter: lockAfter,
lockDuration: lockDuration,
}
}
@ -201,6 +209,46 @@ func (s *IdentityService) UpsertWebSession(user, sid string, session services.We
return trace.Wrap(err)
}
// IncreaseLoginAttempts bumps "login attempt" counter for the given user. If the counter
// reaches 'lockAfter' value, it locks the account and returns access denied error.
func (s *IdentityService) IncreaseLoginAttempts(user string) error {
bucket := []string{"web", "users", user}
data, _, err := s.backend.GetValAndTTL(bucket, "lock")
// unexpected error?
if err != nil && !trace.IsNotFound(err) {
return trace.Wrap(err)
}
// bump the attempt count
if len(data) < 1 {
data = []byte{0}
}
// check the attempt count
if len(data) > 0 && data[0] >= s.lockAfter {
return trace.AccessDenied("this account has been locked for %v", s.lockDuration)
}
newData := []byte{data[0] + 1}
// "create val" will create a new login attempt counter, or it will
// do nothing if it's already there.
//
// "compare and swap" will bump the counter +1
s.backend.CreateVal(bucket, "lock", data, s.lockDuration)
_, err = s.backend.CompareAndSwap(bucket, "lock", newData, s.lockDuration, data)
if err != nil {
panic(err.Error())
}
return trace.Wrap(err)
}
// ResetLoginAttempts resets the "login attempt" counter to zero.
func (s *IdentityService) ResetLoginAttempts(user string) error {
err := s.backend.DeleteKey([]string{"web", "users", user}, "lock")
if trace.IsNotFound(err) {
return nil
}
return trace.Wrap(err)
}
// GetWebSession returns a web session state for a given user and session id
func (s *IdentityService) GetWebSession(user, sid string) (*services.WebSession, error) {
val, err := s.backend.GetVal(
@ -269,23 +317,24 @@ func (s *IdentityService) UpsertPassword(user string,
// CheckPassword is called on web user or tsh user login
func (s *IdentityService) CheckPassword(user string, password []byte, hotpToken string) error {
if err := services.VerifyPassword(password); err != nil {
return trace.Wrap(err)
}
hash, err := s.GetPasswordHash(user)
if err != nil {
return trace.Wrap(err)
}
if err = s.IncreaseLoginAttempts(user); err != nil {
return trace.Wrap(err)
}
if err := bcrypt.CompareHashAndPassword(hash, password); err != nil {
return trace.BadParameter("passwords do not match")
return trace.AccessDenied("passwords do not match")
}
otp, err := s.GetHOTP(user)
if err != nil {
return trace.Wrap(err)
}
if !otp.Scan(hotpToken, defaults.HOTPFirstTokensRange) {
return trace.BadParameter("bad one time token")
return trace.AccessDenied("bad one time token")
}
defer s.ResetLoginAttempts(user)
if err := s.UpsertHOTP(user, otp); err != nil {
return trace.Wrap(err)
}
@ -302,10 +351,13 @@ func (s *IdentityService) CheckPasswordWOToken(user string, password []byte) err
if err != nil {
return trace.Wrap(err)
}
if err = s.IncreaseLoginAttempts(user); err != nil {
return trace.Wrap(err)
}
if err := bcrypt.CompareHashAndPassword(hash, password); err != nil {
return trace.BadParameter("passwords do not match")
}
defer s.ResetLoginAttempts(user)
return nil
}

View file

@ -363,23 +363,23 @@ func (s *ServicesTestSuite) PasswordCRUD(c *C) {
token1 := otp.OTP()
err = s.WebS.CheckPassword("user1", pass, "123456")
c.Assert(trace.IsBadParameter(err), Equals, true, Commentf("%T", err))
c.Assert(trace.IsAccessDenied(err), Equals, true, Commentf("%T", err))
c.Assert(s.WebS.CheckPassword("user1", pass, token1), IsNil)
err = s.WebS.CheckPassword("user1", pass, token1)
c.Assert(trace.IsBadParameter(err), Equals, true, Commentf("%T", err))
c.Assert(trace.IsAccessDenied(err), Equals, true, Commentf("%T", err))
token2 := otp.OTP()
err = s.WebS.CheckPassword("user1", []byte("abc123123"), token2)
c.Assert(trace.IsBadParameter(err), Equals, true, Commentf("%T", err))
c.Assert(trace.IsAccessDenied(err), Equals, true, Commentf("%T", err))
err = s.WebS.CheckPassword("user1", pass, "123456")
c.Assert(trace.IsBadParameter(err), Equals, true, Commentf("%T", err))
c.Assert(trace.IsAccessDenied(err), Equals, true, Commentf("%T", err))
c.Assert(s.WebS.CheckPassword("user1", pass, token2), IsNil)
err = s.WebS.CheckPassword("user1", pass, token1)
c.Assert(trace.IsBadParameter(err), Equals, true, Commentf("%T", err))
c.Assert(trace.IsAccessDenied(err), Equals, true, Commentf("%T", err))
_ = otp.OTP()
_ = otp.OTP()
@ -388,12 +388,12 @@ func (s *ServicesTestSuite) PasswordCRUD(c *C) {
token7 := otp.OTP()
err = s.WebS.CheckPassword("user1", pass, token7)
c.Assert(trace.IsBadParameter(err), Equals, true, Commentf("%T", err))
c.Assert(trace.IsAccessDenied(err), Equals, true, Commentf("%T", err))
c.Assert(s.WebS.CheckPassword("user1", pass, token6), IsNil)
err = s.WebS.CheckPassword("user1", pass, "123456")
c.Assert(trace.IsBadParameter(err), Equals, true, Commentf("%T", err))
c.Assert(trace.IsAccessDenied(err), Equals, true, Commentf("%T", err))
c.Assert(s.WebS.CheckPassword("user1", pass, token7), IsNil)