Account lockout
Progressive lockout after repeated failed passwords or second-factor codes, with an emailed unlock link and admin unlock.
Account lockout
Daywalker Auth locks an account for a while after too many wrong passwords or wrong second-factor codes. It is on for every instance and needs no code on your side.
How it works
- 5 wrong answers in 15 minutes lock the account. Wrong passwords and wrong two-factor codes (authenticator, SMS, backup code, security key) count together toward one lock. A correct answer forgives the failures for that step.
- Locks grow each time: 5 minutes, 15 minutes, 1 hour, 6 hours, then 24 hours. The count is forgotten 24 hours after a lock ends, or when you reset it (see below).
- Attempts are reserved before the secret is checked, so a flood of parallel guesses can't get past the limit.
- While locked, even the right password is refused with
429 account_locked. - Same response whether or not the account exists. An email address with no account is counted and locked exactly like a real one, so the response never tells an attacker which emails are registered. Only real accounts get an email, an audit entry and a webhook.
- Privacy. Counters and lock state in Valkey use HMAC-keyed names (no user IDs or emails in key names). Real accounts also have a row in
account_lockouts(no personal data; the unlock token is stored only as a SHA-256), so a lock survives a Valkey restart.
Passkeys, social sign-in, SSO and email codes are not affected by a lock.
Unlocking
- Emailed link. When an account locks, its owner gets an email ("Account locked") with a single-use link, valid for 24 hours. The link opens a hosted page at
/unlockand needs one click, so mail scanners can't use it up. Using the link keeps the escalation level, so a repeat attacker still gets longer locks. - Password reset. Completing a reset lifts the lock and resets the level.
- You, as the developer. In the dashboard, open Users: locked accounts show a "locked until" badge and an Unlock button. From your backend:
const daywalker = createBackendClient({ secretKey: process.env.DAYWALKER_SECRET_KEY!, apiUrl: "https://users.daywalker.dev" });
const user = await daywalker.users.get(userId); // user.lockedUntil: ISO time or null
await daywalker.users.unlock(userId); // { id, unlocked: true | false }Or POST /v1/users/{userId}/unlock with your secret key. An admin unlock also resets the level.
Custom UI
Handle the account_locked error code (HTTP 429) on signIn and completeSecondFactor. If you build your own unlock page, read #token= from the URL fragment and call:
await auth.unlockAccount(token); // POST /v1/client/unlock; throws AuthError invalid_unlock_link if used or expiredThe email wording is editable under Emails (account_unlock, with {{link}}, {{app_name}} and {{expires_hours}}).
Events
| Where | Name | When |
|---|---|---|
| Webhook + audit log | user.locked |
An account locks. Data: userId, lockedUntil, level, reason (password or second_factor) |
| Webhook + audit log | user.unlocked |
A lock is lifted. Data: userId, reason (email_link, admin, password_reset) |
Nothing is emitted for email addresses that have no account.
Things to know
- Anyone who knows an email address can lock that account for a few minutes by guessing. That's the cost of any lockout; the unlock link and the short first lock keep it mild, and per-IP limits still apply.
- Failed attempts also still count toward the older per-IP and per-email request limits.