Skip to content

feat: add login attempt throttling#4539

Open
veeceey wants to merge 2 commits intoory:masterfrom
veeceey:feat/issue-3037-login-throttling
Open

feat: add login attempt throttling#4539
veeceey wants to merge 2 commits intoory:masterfrom
veeceey:feat/issue-3037-login-throttling

Conversation

@veeceey
Copy link

@veeceey veeceey commented Feb 23, 2026

Adds configurable login throttling to protect against brute force credential attacks. This has been a long-requested feature (#3037, #654).

What it does

Tracks failed login attempts per identity in memory and temporarily locks out accounts that exceed a configurable threshold. Config looks like:

selfservice:
  flows:
    login:
      throttle:
        max_attempts: 5
        window: 5m
        lockout_duration: 15m

When disabled (max_attempts: 0, the default), behavior is unchanged from today.

How it works

  • New throttle.Limiter in selfservice/flow/login/throttle/ does the actual tracking with a sliding window approach
  • On failed login: records the failure, if threshold exceeded returns 429
  • On successful login: clears the failure history
  • Cleanup() method prevents unbounded memory growth
  • Config accessors added to driver/config for the three settings
  • Also added ExtractIdentityID helper in x/err.go to pull identity IDs from wrapped errors

Limitations / future work

This is an in-memory implementation, so it won't persist across restarts and doesn't share state across multiple kratos instances. A database-backed implementation (as outlined in the original issue) would be the next step, but this gets the core logic and config in place.

Tests

$ go test ./selfservice/flow/login/throttle/... -v
=== RUN   TestLimiter_BasicLockout
--- PASS: TestLimiter_BasicLockout (0.00s)
=== RUN   TestLimiter_SuccessResetsCounter
--- PASS: TestLimiter_SuccessResetsCounter (0.00s)
=== RUN   TestLimiter_DifferentIdentities
--- PASS: TestLimiter_DifferentIdentities (0.00s)
=== RUN   TestLimiter_UnknownIdentity
--- PASS: TestLimiter_UnknownIdentity (0.00s)
=== RUN   TestLimiter_Cleanup
--- PASS: TestLimiter_Cleanup (0.02s)
=== RUN   TestDefaultConfig
--- PASS: TestDefaultConfig (0.00s)
PASS

$ go build ./selfservice/flow/login/...
# clean build

Relates to #3037

Implements in-memory login attempt throttling that tracks failed attempts
per identity and temporarily locks accounts after exceeding configurable
thresholds.

New config options under selfservice.flows.login.throttle:
- max_attempts: max failures before lockout (default: 0 = disabled)
- window: time window for counting failures (default: 5m)
- lockout_duration: how long account stays locked (default: 15m)

When throttling is enabled and an identity exceeds max_attempts within
the window, subsequent login attempts return HTTP 429 until the lockout
expires. Successful logins reset the counter.

Relates to ory#3037
@veeceey veeceey requested review from a team and aeneasr as code owners February 23, 2026 03:55
@veeceey
Copy link
Author

veeceey commented Feb 23, 2026

Test Results

$ go build ./selfservice/flow/login/...
# clean

$ go test ./selfservice/flow/login/throttle/... -v
=== RUN   TestLimiter_BasicLockout
--- PASS: TestLimiter_BasicLockout (0.00s)
=== RUN   TestLimiter_SuccessResetsCounter
--- PASS: TestLimiter_SuccessResetsCounter (0.00s)
=== RUN   TestLimiter_DifferentIdentities
--- PASS: TestLimiter_DifferentIdentities (0.00s)
=== RUN   TestLimiter_UnknownIdentity
--- PASS: TestLimiter_UnknownIdentity (0.00s)
=== RUN   TestLimiter_Cleanup
--- PASS: TestLimiter_Cleanup (0.02s)
=== RUN   TestDefaultConfig
--- PASS: TestDefaultConfig (0.00s)
PASS
ok      github.com/ory/kratos/selfservice/flow/login/throttle  0.257s

$ go test ./x/... -run TestWrapWithIdentityIDError -v
--- PASS: TestWrapWithIdentityIDError (0.00s)
    --- PASS: TestWrapWithIdentityIDError/case=wraps_error_with_identity_ID (0.00s)
    --- PASS: TestWrapWithIdentityIDError/case=unwraps_to_original_error (0.00s)
    --- PASS: TestWrapWithIdentityIDError/case=returns_nil_when_wrapping_nil_error (0.00s)
    --- PASS: TestWrapWithIdentityIDError/case=preserves_identity_ID_with_nil_UUID (0.00s)
    --- PASS: TestWrapWithIdentityIDError/case=can_wrap_already_wrapped_error (0.00s)
    --- PASS: TestWrapWithIdentityIDError/case=works_with_errors.Is (0.00s)
PASS

@CLAassistant
Copy link

CLAassistant commented Feb 23, 2026

CLA assistant check
All committers have signed the CLA.

Signed-off-by: Varun Chawla <varun_6april@hotmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants