Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add invalid attempts #43

Open
wants to merge 57 commits into
base: add_request_validators
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
be56c52
added logs
iamazzeez Mar 25, 2022
3137215
added blocked_user talble
iamazzeez Mar 29, 2022
67b0bc9
createblockeduser type fix
iamazzeez Mar 29, 2022
7f0ff86
added test cases
iamazzeez Mar 29, 2022
05d24a6
added test cases
iamazzeez Mar 29, 2022
1fe8fce
added kubernetes storage code
iamazzeez Mar 29, 2022
728a568
added etcd storage code
iamazzeez Mar 29, 2022
8b3712b
migrate stmts
iamazzeez Mar 29, 2022
dc21cd3
migrate only for psql
iamazzeez Mar 29, 2022
11ddfe4
migrate only for psql
iamazzeez Mar 29, 2022
fc31c46
migration fix
iamazzeez Mar 30, 2022
5066d37
test
iamazzeez Mar 30, 2022
1743e3e
test
iamazzeez Mar 30, 2022
8fa56ff
check if user is blocked logic
iamazzeez Mar 30, 2022
1cdf876
migration syntax fix
iamazzeez Mar 30, 2022
69efe69
wip blocked user logic
iamazzeez Mar 30, 2022
9eba331
update counter syntax fix
iamazzeez Mar 30, 2022
21a88e4
added ui render logic
iamazzeez Mar 30, 2022
720100b
added ui render logic fix
iamazzeez Mar 30, 2022
d10e5b5
check if user is blocked condition
iamazzeez Mar 31, 2022
b1da635
check if user is blocked condition fix
iamazzeez Mar 31, 2022
ce0950f
check if user is blocked condition fix
iamazzeez Mar 31, 2022
34cf44e
check if user is blocked condition fix
iamazzeez Mar 31, 2022
b68997b
delete user on successful login
iamazzeez Mar 31, 2022
8a9bca6
delete user on successful login fix
iamazzeez Mar 31, 2022
f54f96e
add config from automate
iamazzeez Apr 1, 2022
55455ae
typo fix
iamazzeez Apr 1, 2022
b830d59
added invalidAttempts config to server
iamazzeez Apr 1, 2022
0ab2774
added invalidAttempts config to server fix
iamazzeez Apr 1, 2022
d6fa7b7
maxAllowedAttempts type fix
iamazzeez Apr 1, 2022
eb46ede
maxAllowedAttempts send to ui
iamazzeez Apr 1, 2022
aee619c
maxAllowedAttempts send to ui fix
iamazzeez Apr 1, 2022
8312d75
blockDuration send to ui fix
iamazzeez Apr 1, 2022
256a595
removed unnecessary commits
iamazzeez Apr 4, 2022
81d15a7
code refactored
iamazzeez Apr 5, 2022
bd0c6d9
code refactor
iamazzeez Apr 5, 2022
4e5c55c
renamed BlockedUser to InvalidLoginAttempt
iamazzeez Apr 11, 2022
b44b303
config terminology update.
iamazzeez Apr 12, 2022
fbc74d2
sql row name update
iamazzeez Apr 12, 2022
bc65a45
debug stmts
iamazzeez Apr 12, 2022
c1fe8c0
removed logs
iamazzeez Apr 13, 2022
bc5ae47
enable/disable invalid_login_attempts
iamazzeez Apr 13, 2022
c94d07d
added logs
iamazzeez Apr 14, 2022
55798a5
removeed logs
iamazzeez Apr 14, 2022
b162f79
ldap invalidattempts handle fix
iamazzeez Apr 25, 2022
258273b
handle duplicate entries in invalid_attempts table
iamazzeez Apr 25, 2022
5ceaece
handleInvalidLoginAttempts update for ldap
iamazzeez Apr 25, 2022
f6cd315
error fix
iamazzeez Apr 25, 2022
83cd8b8
error fix
iamazzeez Apr 25, 2022
70d1856
changed blockedDuration to blockedDurationInMinutes
iamazzeez Apr 26, 2022
f5f1c79
reverted if not exists from create table
iamazzeez Apr 28, 2022
b8e1a0b
Merge pull request #53 from chef/ldap_fix
Dmaddu Apr 28, 2022
aacac1d
updated_at time
iamazzeez Apr 29, 2022
b5b1255
updatedat fix
iamazzeez Apr 30, 2022
492576f
Merge pull request #56 from chef/ldap_fix
Dmaddu May 2, 2022
6c1614c
Sanju/test refresh (#81)
SanjuPal01 Jul 26, 2022
ef6613f
pq pkg upgrade (#151)
iamazzeez Jun 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions cmd/dex/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ import (

// Config is the config format for the main application.
type Config struct {
Issuer string `json:"issuer"`
Storage Storage `json:"storage"`
Web Web `json:"web"`
Telemetry Telemetry `json:"telemetry"`
OAuth2 OAuth2 `json:"oauth2"`
GRPC GRPC `json:"grpc"`
Expiry Expiry `json:"expiry"`
Logger Logger `json:"logger"`
Issuer string `json:"issuer"`
Storage Storage `json:"storage"`
Web Web `json:"web"`
Telemetry Telemetry `json:"telemetry"`
OAuth2 OAuth2 `json:"oauth2"`
GRPC GRPC `json:"grpc"`
Expiry Expiry `json:"expiry"`
InvalidAttempts InvalidAttempts `json:"invalidAttempts`
Logger Logger `json:"logger"`

Frontend server.WebConfig `json:"frontend"`

Expand Down Expand Up @@ -291,3 +292,9 @@ type Logger struct {
// Format specifies the format to be used for logging.
Format string `json:"format"`
}

type InvalidAttempts struct {
EnableInvalidAttempts bool `json:"enableInvalidAttempts"`
BlockDuration int32 `json:"blockDuration"`
MaxAttemptsAllowed int32 `json:"maxAttemptsAllowed"`
}
7 changes: 7 additions & 0 deletions cmd/dex/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,13 @@ func serve(cmd *cobra.Command, args []string) error {
logger.Infof("config id tokens valid for: %v", idTokens)
serverConfig.IDTokensValidFor = idTokens
}

if c.InvalidAttempts.EnableInvalidAttempts {
serverConfig.EnableInvalidAttempts = c.InvalidAttempts.EnableInvalidAttempts
serverConfig.BlockDuration = c.InvalidAttempts.BlockDuration
serverConfig.MaxAttemptsAllowed = c.InvalidAttempts.MaxAttemptsAllowed
}

if c.Expiry.AuthRequests != "" {
authRequests, err := time.ParseDuration(c.Expiry.AuthRequests)
if err != nil {
Expand Down
112 changes: 110 additions & 2 deletions server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,53 @@ func (s *Server) discoveryHandler() (http.HandlerFunc, error) {
}), nil
}

func (s *Server) isFailedAttemptDurationExceeded(u storage.BlockedUser) bool {
if diff := time.Since(u.UpdatedAt); diff.Minutes() > float64(s.blockDuration) {
return true
}
return false
}

func (s *Server) resetFailedAttempt(username string, w http.ResponseWriter, r *http.Request) {
updater := func(u storage.BlockedUser) (storage.BlockedUser, error) {
u.InvalidAttemptsCount = 1
u.UpdatedAt = time.Now()
return u, nil
}

if err := s.storage.UpdateBlockedUser(username, updater); err != nil {
s.logger.Errorf("Failed to reset invalid counter: %v", err)
s.renderError(r, w, http.StatusInternalServerError, fmt.Sprintf("db error: %v", err))
return
}
}

func (s *Server) isAllowedFailedAttemptExceeded(u storage.BlockedUser) bool {
return u.InvalidAttemptsCount >= s.maxAttemptsAllowed
}

func (s *Server) isUserBlocked(u storage.BlockedUser) bool {
diff := time.Since(u.UpdatedAt)
if diff.Minutes() <= float64(s.blockDuration) && u.InvalidAttemptsCount >= s.maxAttemptsAllowed {
return true
}
return false
}

func (s *Server) updateInvalidAttemptCount(username string, w http.ResponseWriter, r *http.Request) {
updater := func(u storage.BlockedUser) (storage.BlockedUser, error) {
u.InvalidAttemptsCount = u.InvalidAttemptsCount + 1
u.UpdatedAt = time.Now()
return u, nil
}

if err := s.storage.UpdateBlockedUser(username, updater); err != nil {
s.logger.Errorf("Failed to increment invalid counter: %v", err)
s.renderError(r, w, http.StatusInternalServerError, fmt.Sprintf("db error: %v", err))
return
}
}

// handleAuthorization handles the OAuth2 auth endpoint.
func (s *Server) handleAuthorization(w http.ResponseWriter, r *http.Request) {
authReq, err := s.parseAuthorizationRequest(r)
Expand Down Expand Up @@ -351,7 +398,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
}
http.Redirect(w, r, callbackURL, http.StatusFound)
case connector.PasswordConnector:
if err := s.templates.password(r, w, r.URL.String(), "", usernamePrompt(conn), false, showBacklink); err != nil {
if err := s.templates.password(r, w, r.URL.String(), "", usernamePrompt(conn), false, showBacklink, 0, s.maxAttemptsAllowed, s.blockDuration); err != nil {
s.logger.Errorf("Server template error: %v", err)
}
case connector.SAMLConnector:
Expand Down Expand Up @@ -392,25 +439,86 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
username := r.FormValue("login")
password := r.FormValue("password")

//check if username is in blocked_user table
blockedUser, err := s.storage.GetBlockedUser(username)
if err != nil {
s.logger.Errorf("Failed to get blocked user: %v", err)
s.renderError(r, w, http.StatusInternalServerError, fmt.Sprintf("Failed to get blocked user: %v", err))
return
}

if s.isUserBlocked(blockedUser) {
s.logger.Errorf("User is blocked")
if err := s.templates.password(r, w, r.URL.String(), username, usernamePrompt(passwordConnector), true, showBacklink, blockedUser.InvalidAttemptsCount, s.maxAttemptsAllowed, s.blockDuration); err != nil {
s.logger.Errorf("Server template error: %v", err)
}
return
}

identity, ok, err := passwordConnector.Login(r.Context(), scopes, username, password)
if err != nil {
s.logger.Errorf("Failed to login user: %v", err)
s.renderError(r, w, http.StatusInternalServerError, fmt.Sprintf("Login error: %v", err))
return
}
if !ok {
if err := s.templates.password(r, w, r.URL.String(), username, usernamePrompt(passwordConnector), true, showBacklink); err != nil {
if blockedUser.Username != username {
//create blocked user
err := s.storage.CreateBlockedUser(storage.BlockedUser{
Username: username,
InvalidAttemptsCount: 1,
UpdatedAt: time.Now(),
})

if err != nil {
s.logger.Errorf("Failed to create blocked user: %v", err)
s.renderError(r, w, http.StatusInternalServerError, "db error.")
return
}

if err := s.templates.password(r, w, r.URL.String(), username, usernamePrompt(passwordConnector), true, showBacklink, 1, s.maxAttemptsAllowed, s.blockDuration); err != nil {
s.logger.Errorf("Server template error: %v", err)
}
return
}

if s.isFailedAttemptDurationExceeded(blockedUser) {
s.resetFailedAttempt(username, w, r)
if err := s.templates.password(r, w, r.URL.String(), username, usernamePrompt(passwordConnector), true, showBacklink, 1, s.maxAttemptsAllowed, s.blockDuration); err != nil {
s.logger.Errorf("Server template error: %v", err)
}
return
}

if s.isAllowedFailedAttemptExceeded(blockedUser) {
s.logger.Errorf("User is blocked: %v", blockedUser.InvalidAttemptsCount)
if err := s.templates.password(r, w, r.URL.String(), username, usernamePrompt(passwordConnector), true, showBacklink, blockedUser.InvalidAttemptsCount, s.maxAttemptsAllowed, s.blockDuration); err != nil {
s.logger.Errorf("Server template error: %v", err)
}
return
}

s.updateInvalidAttemptCount(username, w, r)

if err := s.templates.password(r, w, r.URL.String(), username, usernamePrompt(passwordConnector), true, showBacklink, blockedUser.InvalidAttemptsCount+1, s.maxAttemptsAllowed, s.blockDuration); err != nil {
s.logger.Errorf("Server template error: %v", err)
}
return
}

redirectURL, err := s.finalizeLogin(identity, authReq, conn.Connector)
if err != nil {
s.logger.Errorf("Failed to finalize login: %v", err)
s.renderError(r, w, http.StatusInternalServerError, "Login error.")
return
}

if err := s.storage.DeleteBlockedUser(username); err != nil && err != storage.ErrNotFound {
s.logger.Errorf("Failed to delete blocked user: %v", err)
s.renderError(r, w, http.StatusInternalServerError, "db error.")
return
}

http.Redirect(w, r, redirectURL, http.StatusSeeOther)
default:
s.renderError(r, w, http.StatusBadRequest, "Unsupported request method.")
Expand Down
11 changes: 11 additions & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ type Config struct {
IDTokensValidFor time.Duration // Defaults to 24 hours
AuthRequestsValidFor time.Duration // Defaults to 24 hours
DeviceRequestsValidFor time.Duration // Defaults to 5 minutes

EnableInvalidAttempts bool
BlockDuration int32
MaxAttemptsAllowed int32
// If set, the server will use this connector to handle password grants
PasswordConnector string

Expand Down Expand Up @@ -162,6 +166,10 @@ type Server struct {
authRequestsValidFor time.Duration
deviceRequestsValidFor time.Duration

enableInvalidAttempts bool
blockDuration int32
maxAttemptsAllowed int32

logger log.Logger
}

Expand Down Expand Up @@ -236,6 +244,9 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy)
templates: tmpls,
passwordConnector: c.PasswordConnector,
logger: c.Logger,
enableInvalidAttempts: c.EnableInvalidAttempts,
blockDuration: c.BlockDuration,
maxAttemptsAllowed: c.MaxAttemptsAllowed,
}

// Retrieves connector objects in backend storage. This list includes the static connectors
Expand Down
19 changes: 11 additions & 8 deletions server/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,15 +284,18 @@ func (t *templates) login(r *http.Request, w http.ResponseWriter, connectors []c
return renderTemplate(w, t.loginTmpl, data)
}

func (t *templates) password(r *http.Request, w http.ResponseWriter, postURL, lastUsername, usernamePrompt string, lastWasInvalid, showBacklink bool) error {
func (t *templates) password(r *http.Request, w http.ResponseWriter, postURL, lastUsername, usernamePrompt string, lastWasInvalid, showBacklink bool, invalidAttemptsCount int32, maxAttemptsAllowed int32, blockDuration int32) error {
data := struct {
PostURL string
BackLink bool
Username string
UsernamePrompt string
Invalid bool
ReqPath string
}{postURL, showBacklink, lastUsername, usernamePrompt, lastWasInvalid, r.URL.Path}
PostURL string
BackLink bool
Username string
UsernamePrompt string
Invalid bool
ReqPath string
InvalidAttemptsCount int32
MaxAttemptsAllowed int32
BlockDuration int32
}{postURL, showBacklink, lastUsername, usernamePrompt, lastWasInvalid, r.URL.Path, invalidAttemptsCount, maxAttemptsAllowed, blockDuration}
return renderTemplate(w, t.passwordTmpl, data)
}

Expand Down
43 changes: 41 additions & 2 deletions storage/etcd/etcd.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const (
refreshTokenPrefix = "refresh_token/"
authRequestPrefix = "auth_req/"
passwordPrefix = "password/"
blockedUserPrefix = "blocked_user/"
offlineSessionPrefix = "offline_session/"
connectorPrefix = "connector/"
keysName = "openid-connect-keys"
Expand Down Expand Up @@ -283,13 +284,26 @@ func (c *conn) CreatePassword(p storage.Password) error {
return c.txnCreate(ctx, passwordPrefix+strings.ToLower(p.Email), p)
}

func (c *conn) CreateBlockedUser(u storage.BlockedUser) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout)
defer cancel()
return c.txnCreate(ctx, blockedUserPrefix+strings.ToLower(u.Username), u)
}

func (c *conn) GetPassword(email string) (p storage.Password, err error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout)
defer cancel()
err = c.getKey(ctx, keyEmail(passwordPrefix, email), &p)
return p, err
}

func (c *conn) GetBlockedUser(username string) (u storage.BlockedUser, err error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout)
defer cancel()
err = c.getKey(ctx, keyUsername(blockedUserPrefix, username), &u)
return u, err
}

func (c *conn) UpdatePassword(email string, updater func(p storage.Password) (storage.Password, error)) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout)
defer cancel()
Expand All @@ -308,12 +322,36 @@ func (c *conn) UpdatePassword(email string, updater func(p storage.Password) (st
})
}

func (c *conn) UpdateBlockedUser(username string, updater func(p storage.BlockedUser) (storage.BlockedUser, error)) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout)
defer cancel()
return c.txnUpdate(ctx, keyEmail(blockedUserPrefix, username), func(currentValue []byte) ([]byte, error) {
var current storage.BlockedUser
if len(currentValue) > 0 {
if err := json.Unmarshal(currentValue, &current); err != nil {
return nil, err
}
}
updated, err := updater(current)
if err != nil {
return nil, err
}
return json.Marshal(updated)
})
}

func (c *conn) DeletePassword(email string) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout)
defer cancel()
return c.deleteKey(ctx, keyEmail(passwordPrefix, email))
}

func (c *conn) DeleteBlockedUser(username string) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout)
defer cancel()
return c.deleteKey(ctx, keyEmail(blockedUserPrefix, username))
}

func (c *conn) ListPasswords() (passwords []storage.Password, err error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout)
defer cancel()
Expand Down Expand Up @@ -558,8 +596,9 @@ func (c *conn) txnUpdate(ctx context.Context, key string, update func(current []
return nil
}

func keyID(prefix, id string) string { return prefix + id }
func keyEmail(prefix, email string) string { return prefix + strings.ToLower(email) }
func keyID(prefix, id string) string { return prefix + id }
func keyEmail(prefix, email string) string { return prefix + strings.ToLower(email) }
func keyUsername(prefix, username string) string { return prefix + strings.ToLower(username) }
func keySession(prefix, userID, connID string) string {
return prefix + strings.ToLower(userID+"|"+connID)
}
Expand Down
Loading