mirror of
https://github.com/gravitational/teleport
synced 2024-10-23 02:32:39 +00:00
Merge pull request #428 from gravitational/ev/lock
Account lock after N unsuccessful login attempts
This commit is contained in:
commit
032e743ca6
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in a new issue