Skip to main content

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.

Written for whoever runs IT9 min readUpdated

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 rejection messages, verbatim
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 character

Password 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
Response to a sign-in on a locked account

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.

The other refusals on the sign-in path, so you can tell them apart when somebody reports one.
What happenedMessageStatus
Wrong password, or no such accountinvalid email or password401
Correct password, wrong one-time codeinvalid MFA code401
The account has not confirmed its email addressemail verification required403
A policy rule denied itThe rule's own message — for example Sign-in from your current network is not permitted.403
The policy engine could not answerSign-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.

The two rate-limit buckets, and what each one covers.
BucketLimitEndpoints
Public authentication10 requests per minuteRegistration, accepting an invitation, signing in, provider discovery, starting an SSO sign-in, refreshing a session, and completing a second-factor challenge.
Recovery and verification5 requests per minuteRequesting 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:

The 429 body from the rate limiter, served as text/plain
{"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

  1. Stop worrying about password complexity and go and check who has a second factor: Multi-factor authentication factors.
  2. Add an alert on bursts of auth.login_failed against several accounts from one address — that pattern is the one worth waking up for.
  3. Read Session policy and sign-out behaviour so you know what a password reset does and does not end.

Was this article wrong?

If a procedure here does not match what you see, or a limit we described has changed, tell us and we will fix the page. Email us about this article, or see how to get help if you need an answer rather than a correction.

Everything in identity and access