Password policy, lockout and rate limits
The rules a password must satisfy, what happens after five wrong attempts, the limits on the public sign-in endpoints, and the exact messages people see.
Passwords are the control most people think they understand and most vendors describe least precisely. This article gives the actual rules a password must satisfy, what happens when somebody gets it wrong repeatedly, the limits on the endpoints an attacker would target, and the exact wording of the messages your colleagues will read out to you over the phone.
It also names a place where the product contradicts itself, because a number you might read in one screen is not the number the sign-in path uses.
What a password must satisfy
The authentication path applies these, and they are fixed in the platform rather than configurable:
- At least twelve characters.
- At least one uppercase letter.
- At least one lowercase letter.
- At least one digit.
- At least one punctuation mark or symbol.
- Not one of the last five passwords on the account.
Passwords are stored using bcrypt at a work factor of 12, which is a deliberate cost: verifying one takes long enough to make offline guessing expensive if the hashes were ever stolen.
The rejection messages are specific about which rule failed, which is worth knowing because a colleague will paste one at you:
password does not meet policy requirements: must be at least 12 characters
password does not meet policy requirements: must contain at least one uppercase letter
password does not meet policy requirements: must contain at least one lowercase letter
password does not meet policy requirements: must contain at least one digit
password does not meet policy requirements: must contain at least one special characterPassword ageing exists in the policy object with a default of ninety days. Whether the sign-in path enforces expiry is something we have not verified, so we are not going to tell you it does. Do not build a compliance answer on forced rotation here; modern guidance is against scheduled rotation anyway, in favour of rotating on evidence of compromise.
Reuse
The last five passwords are kept as hashes and a new password is compared against them, along with the current one. A match is refused with cannot reuse a recent password and HTTP 400, and the refusal is audited — as auth.password_reuse_rejected when somebody changes their own, and as auth.admin_password_reset_reuse_rejected when an administrator resets somebody else's.
This matters more than it sounds. The most common reaction to a forced reset is to cycle straight back to the password that prompted it, which returns the account to the exact state you were trying to leave.
Lockout
Five failed sign-in attempts lock the account for fifteen minutes. The counter resets on a successful sign-in. The lock clears by itself when the fifteen minutes have passed; an administrator can also clear it by setting the account back to active, which resets the failed-attempt counter at the same time. A locked-out person can now also clear it themselves, without waiting or asking anyone — see Self-service account unlock, which runs the identical reactivation path the admin unlock action uses.
A locked account answers with HTTP 429 — not 401 or 403 — and this message:
account temporarily locked due to too many failed attempts
The 429 is unusual and worth noting if you are writing a client: everywhere else a refused sign-in is a 401. Four audit actions cover this ground — auth.login_failed, auth.account_locked, auth.login_locked and auth.login_locked_out — and a burst of them against several accounts from one address is the signature of a password-spraying attempt.
| What happened | Message | Status |
|---|---|---|
| Wrong password, or no such account | invalid email or password | 401 |
| Correct password, wrong one-time code | invalid MFA code | 401 |
| The account has not confirmed its email address | email verification required | 403 |
| A policy rule denied it | The rule's own message — for example Sign-in from your current network is not permitted. | 403 |
| The policy engine could not answer | Sign-in is temporarily unavailable. Please try again. | 503 |
The first row is one message for two different situations on purpose. Telling a caller that the address exists but the password is wrong turns the sign-in endpoint into a way of enumerating your staff, which is the reconnaissance step before a spraying campaign.
Rate limits on the public endpoints
Lockout protects a single account. Rate limiting protects the endpoints themselves, and there are two buckets, both counted per client address.
| Bucket | Limit | Endpoints |
|---|---|---|
| Public authentication | 10 requests per minute | Registration, accepting an invitation, signing in, provider discovery, starting an SSO sign-in, refreshing a session, and completing a second-factor challenge. |
| Recovery and verification | 5 requests per minute | Requesting and confirming email verification, forgotten password, and resetting a password with a token. |
The tighter bucket on the second group is deliberate: a forgotten-password endpoint is both a way of testing whether an address has an account and a way of sending mail to somebody on your behalf. Forgotten password now answers with HTTP 429 after five attempts in a minute. Whether that endpoint mints a token at all, and how long the link stays valid once it does, is now also an organisation-level choice — see Account recovery policy.
Three honest caveats about what the limiter is worth today:
- It counts in fixed windows, not sliding ones. Once a window elapses the allowance refills wholesale, so a caller can use its full allowance at the end of one window and again at the start of the next.
- It is held in memory on each running instance. Where the service runs as several instances behind a load balancer, the effective allowance is the configured figure multiplied by the number of instances, because each keeps its own count. This is the same class of limitation that has already been fixed elsewhere in the platform by moving the state into the database, and it has not yet been fixed here.
- It raises the cost of a bulk attack; it does not stop a determined one. Read it as a blunt instrument that makes high-volume guessing expensive, and let account lockout, a second factor and the risk engine do the work of stopping a targeted attempt.
One integration detail if you are writing a client against the API. The rate-limit response is the one place in this module where a JSON body is served with a plain-text content type, and it carries no error code alongside the message:
{"error":"rate limit exceeded"}A strict client that trusts the content-type header will fail to parse it. Handle a 429 from the public endpoints as a special case rather than assuming the ordinary error shape.
Changing and resetting a password
Changing your own password requires the current one. Getting it wrong answers current password is incorrect with HTTP 401. On success, every one of that account's sessions is removed, so all their devices sign in again — which is the behaviour you want, since one reason to change a password is that somebody else may know it.
An administrator resetting somebody else's password does the same to the target's sessions, and is refused if the target is themselves. To change your own password you use the ordinary route with your current password; if you have forgotten it, use the recovery flow, which lives in the tighter rate-limit bucket above. The errors are new_password is required, insufficient permissions at 403 where the caller lacks the standing, and user not found at 404.
One further caution about the password-policy object itself: we cannot currently tell you with confidence which permission guards it, because two implementations of the same endpoint exist in the codebase and both claim the same path. Until that is resolved, treat the policy object as something to read rather than to rely on, and remember that the values that actually govern sign-in are the fixed ones at the top of this article.
What to do next
- Stop worrying about password complexity and go and check who has a second factor: Multi-factor authentication factors.
- Add an alert on bursts of
auth.login_failedagainst several accounts from one address — that pattern is the one worth waking up for. - Read Session policy and sign-out behaviour so you know what a password reset does and does not end.