Contents
The login screen must help the legitimate user get in without handing useful information to an attacker. It is a critical entry point from a security standpoint.
A message that's too precise helps the user, but also informs the attacker. A message that's too vague protects the system, but loses the user. The design of the authentication journey has to reconcile these two requirements, and to do so it's necessary to consider the journey as a whole: input, error, blocking, help, the second factor, the session.
Note: this article draws on NIST's authentication requirements1 and on several OWASP practical guides, covering login2, the session3, password reset4 and the second factor5.
NIST uses a precise normative vocabulary. I've reused these terms, as tags, to keep some reference points:
- SHALL an obligation, to be followed strictly.
- SHOULD a strong recommendation, followed unless there's a serious reason not to.
- MAY a mere option left to the designer.
The negative forms SHALL NOT and SHOULD NOT work the same way, in reverse.
The "identifier-first" flow
Sometimes, asking for the identifier first lets you steer the user toward the right method rather than forcing them to choose:
- The journey happens in 2 steps: "1. who are you?" then "2. prove it".
- Depending on the identifier entered, the screen offers the organization's SSO, an already-registered passkey, or the password.
This convenience comes at a known cost: by revealing that an account exists before asking for the secret, the two-step flow makes enumeration possible. It is therefore only acceptable when paired with the protections described later, which strip the attacker of any interest in enumerating.
Do
- Tailor the second screen to the identifier (SSO, passkey, password)
- Enable the conditional UI for passkeys from step 1
- Keep the entered identifier between the two steps
- Pair the flow with rate limiting
Don't
- Explicitly confirm "this account exists" / "this account is unknown"
- Deploy it without protection against mass attempts
- Force the user to choose their method before you know who they are
- Start from scratch and make them re-enter their address at step 2
The fields
A properly configured email field opens the "right" keyboard and disables autocorrect, which would otherwise try to "fix" the address being entered. A one-time-code field can, for its part, suggest the code received by SMS directly, with no round trip to the messaging app.
<!-- Email: right keyboard, no autocorrect or auto-capitalization -->
<input
type="email"
inputmode="email"
autocapitalize="off"
autocorrect="off"
autocomplete="username">
<!-- OTP code: automatic suggestion of the code received by SMS -->
<input
type="text"
inputmode="numeric"
autocomplete="one-time-code">
The states
A few good practices to keep in mind regarding micro-states:
- Disable the button as soon as it's clicked and show progress, to prevent double submission and reassure the user.
- Validate at the right moment: on leaving the field, never on every keystroke, and don't show an error before the field has been left at least once.
- Preserve the input: after a rejected password, never clear the email field. Making the user re-enter the address on every attempt is a gratuitous punishment.
- Flag Caps Lock: a discreet warning on the password field prevents unexplained failures.
Do
- Type the fields (
type,inputmode,autocomplete) - Disable the button and show progress after the click
- Validate on leaving the field, not on every keystroke
- Keep the identifier after a failure
- Warn if Caps Lock is on
Don't
- An email field without
inputmodeor disabled autocorrect - An error shown on the first keystroke
- A button left active during processing, opening the door to double submission
- Clearing the email field after a wrong password
The password
On the password itself, NIST favors length over complexity1:
- SHALL A password used on its own is at least fifteen characters long.
- SHOULD The system accepts at least sixty-four.
- SHALL NOT Imposed composition rules — an uppercase letter, a digit, a special character — are set aside. They push toward predictable patterns without any gain in security.
A detailed article on passwords is available here.
Error messages
When a login fails, the temptation is to help the user by being specific.
"Incorrect password" tells them their identifier is right. "This identifier doesn't exist" tells them the opposite. In both cases, the interface has just confirmed a piece of information to whoever is asking.
An attacker tests a list of addresses and sorts the ones that "exist" from the ones that don't. This is account enumeration. OWASP recommends a generic message, identical regardless of the actual reason for the failure2. Avoid the likes of "unknown identifier", "wrong password", "account disabled or locked", and instead use:
- "if this address is in our database, we'll send you an email"
- "invalid username or password"
Don't
- Incorrect password
- This identifier doesn't exist
- Account disabled
- A different HTTP code or delay depending on the case
Do
- Incorrect username or password
- The same message for every failure reason
- The same HTTP code and the same response time
- For reset: If this address is known, a link has just been sent4
Should you hide whether an account exists?
This neutrality has a drawback, which OWASP frames as a user-experience problem2: a generic message can disorient a well-meaning person who doesn't understand what's wrong and ends up giving up. The trade-off therefore depends on how critical the application and its data are.
That's why some services deliberately choose the opposite. Two-step logins — address first, then password — often reveal that an account exists before even asking for the secret. This choice improves the journey, but it makes enumeration possible. You can only accept it if other protections prevent anyone from profiting from it. Preventing mass attempts strips enumeration of its value, since an attacker can no longer test thousands of addresses2.
For example:
- Rate limiting: after X attempts, the IP is blocked
- CAPTCHA: enumerating thousands of addresses becomes humanly impossible
- Mandatory MFA: even if the attacker knows an account exists, they can't log in without the second factor
- Monitoring: detect and alert on enumeration patterns
Limiting attempts without punishing the user
Locking an account after a few failures seems prudent, but the lockout can backfire on the user. If anyone can lock an account by entering three wrong passwords, the block becomes a denial-of-service tool against the legitimate user.
NIST recommends rate limiting rather than a hard lockout1. The system limits consecutive failed attempts to a hundred per account at most, after which the authenticator is disabled. SHALL That number is a ceiling, not a target: it can be lowered. MAY An honest user rarely goes beyond a few tries, whereas an automated attack tries thousands. Capping at a hundred stops the latter without bothering the former.
NIST describes three ways to reduce the risk of locking out a legitimate user1. First, a bot-detection test before the attempt (reCAPTCHA v3 or Turnstile). Next, a wait that grows with each failure — for example from thirty seconds up to an hour as you approach the ceiling. Finally, analysis of warning signals: an attempt from an unusual IP address or country, or a typing cadence too fast for a human. When one of these signals appears, the system hardens verification — through a bot-detection test or an additional step — or refuses the attempt. After a successful login, the failure counter resets to zero. SHOULD
The iPhone unlock code applies this principle. After a few errors, the device imposes a wait, then lengthens it with each new failure — for example one minute, then five, then fifteen — instead of locking permanently. NIST describes the same pattern for biometric factors1: at most five attempts, ten with attack detection, then a delay of at least thirty seconds before each new try, with an overall ceiling. The principle stays the same: a delay that grows, not a permanent block.
Do
- Cap at a hundred failed attempts per account, or fewer SHALL
- Lengthen the delay after each failure, for example 30 seconds up to 1 hour
- Add a bot-detection test (reCAPTCHA v3 or Turnstile) and risk signals (IP, pace)
- Reset the counter to zero after a successful login SHOULD
- Log failures to detect attacks
Don't
- A permanent lockout triggered by failures
- A hard block, with no announced delay or instructions
- A threshold so low that an honest user gets caught
- A block message that reveals the exact state of the account
On the interface side, indicating a temporary slowdown and the steps to take is better than a hard block with no explanation. The user must understand that they'll be able to try again, and how, without the text revealing the exact state of the account.
Helping after a failure
After a failure, the interface must guide the user without reopening the door to information leakage.4.
- A "Forgot password?" link: kept visible and accessible at all times
- A discreet counter: "2 attempts left before a waiting period" warns without blocking abruptly
- A slowdown message: "Too many attempts. Try again in 30 seconds." with a visible timer if possible
Several other aids reduce failures upstream, and NIST ranks them by strength1:
- SHALL Allowing password managers and autofill is mandatory.
- SHOULD Allowing pasting into the field is recommended, when autofill isn't available.
- SHOULD Offering a button to reveal the entered password is recommended too.
These good practices reduce friction at input and, combined with a password manager, encourage stronger credentials.
<!-- Field that helps the password manager and autofill -->
<label for="password">Password</label>
<input
id="password"
type="password"
autocomplete="current-password"
aria-describedby="pw-help">
<button type="button" aria-pressed="false">Show</button>
<p id="pw-help">Pasting and password managers are accepted.</p>
When the response must stay fully neutral, OWASP suggests systematically redirecting to a help or support page on failure2.
After login: the session
Once the user is recognized, it's the session that keeps them logged in, and it's the session that becomes the target. The first measure is to regenerate the session identifier right after login3.
The session's lifetime rests on two combined expirations: an idle expiration, which closes the session after a period of inactivity, and an absolute expiration, which closes it after a fixed duration no matter what3.
Idle. OWASP advises two to five minutes for a high-stakes application, fifteen to thirty minutes for low risk3. NIST sets this threshold at one hour maximum at AAL2, fifteen minutes at AAL31.
Absolute. OWASP proposes four to eight hours for a workday3. NIST caps it at twenty-four hours at AAL2, twelve hours at AAL31.
Server side. At expiry, invalidation happens server-side, not only client-side. And for high-impact actions — a transfer or a password change — a one-off re-authentication remains justified, even within an open session3.
Accessibility is not optional
Since WCAG 2.2, this is no longer good intent but a standardized criterion, on the same footing as the NIST requirements cited above.
Criterion 3.3.8 Accessible Authentication (Minimum), level AA, prohibits imposing a cognitive function test (memorizing, transcribing, solving a puzzle) at a step of the login, unless an alternative is provided8. Direct consequence: allowing password managers, autofill and pasting is no longer a mere convenience — it's what makes the journey compliant. The earlier point about autofill therefore becomes an accessibility obligation, not a nicety.
A classic CAPTCHA (retyping distorted characters, identifying images) is a cognitive function test, and therefore an accessibility obstacle. This is precisely why invisible solutions, based on risk rather than on a challenge (reCAPTCHA v3, Turnstile), are preferable: they hold back automation without demanding any effort from the user.
Three points round out the picture, in connection with the previous sections:
- Announce errors. A visual error message stays invisible to a screen reader if it isn't signaled. The container must carry an alert role, and the focus must move to the message.
- Don't rely on color alone. Your "Do / Don't" blocks and your error states must remain understandable without color: an icon, a word, a shape.
- Adjustable time limits. Criterion 2.2.1 (level A) requires warning and being able to extend a time limit8: this is exactly the role of the "your session expires in 2 minutes" alert described below.
<!-- Error region announced to assistive technologies -->
<div
id="login-error"
role="alert"
aria-live="assertive"></div>
<!-- Focus moves to this container after the failure -->
Do
- Allow password managers, autofill and pasting (requirement 3.3.8)
- Prefer an invisible bot check over a challenge CAPTCHA
- Announce errors via an alert role and move the focus
- Warn and allow extension before expiry (2.2.1)
Don't
- Block pasting "for security"
- A CAPTCHA to retype or solve, with no alternative
- An error signaled by the field's color alone
- A session that expires with no warning or way to extend
Measuring authentication UX
Authentication leaves traces you can measure to improve the experience:
- First-attempt success rate. The proportion of logins completed without error or retry.
- Time-to-auth. The time elapsed between arriving on the screen and actually gaining access. A lengthening signals a complex journey.
- Password reset rate. A high rate often reflects forgotten credentials for lack of a password manager, or a journey that forces people to create too many.
These measurements must stay aggregated and anonymous: you track rates and durations, never the detail of one individual's attempts. And in keeping with the neutral-response principle seen above, the failure logs useful for attack detection1 are not the same as those you expose in a product dashboard.
Do
- Track first-attempt success and time-to-auth over time
- Treat the reset rate as a friction signal
- Measure MFA enrollment drop-off, method by method
- Cross-reference support tickets with the screens involved
Don't
- Judge authentication UX by intuition or isolated feedback
- Track the detail of a named user's attempts
- Conflate security logs with product measurement
- Freeze the convenience / security slider once and for all
As a reminder, a neutral login is worthless if sign-up replies "this email is already in use", if "forgot password" says "no account found", or if responses differ in HTTP code, size or time. Three reflexes:
- Consistency across all surfaces: login, sign-up and reset return the same kind of neutral response.
- Equalize response times: run a dummy hash for non-existent accounts, otherwise the clock gives away existence even with identical messages.
- Throttle abuse: rate limiting by IP and by identifier, bot detection, progressive hardening (CAPTCHA past a threshold).
Sources & references
- NIST SP 800-63B-4, Digital Identity Guidelines: Authentication and Authenticator Management, 2024. Useful sections: rate limiting 3.2.2, phishing resistance 3.2.5, passwords 3.1.1.2, restricted authenticators 3.2.9, re-authentication and sessions Sec. 2.
- OWASP, Authentication Cheat Sheet. cheatsheetseries.owasp.org
- OWASP, Session Management Cheat Sheet. Useful sections: identifier regeneration, cookie attributes, expiration.
- OWASP, Forgot Password Cheat Sheet. cheatsheetseries.owasp.org
- OWASP, Multifactor Authentication Cheat Sheet. cheatsheetseries.owasp.org
- OWASP, Credential Stuffing Prevention Cheat Sheet. cheatsheetseries.owasp.org
- OWASP, Unvalidated Redirects and Forwards Cheat Sheet. cheatsheetseries.owasp.org
- W3C, Web Content Accessibility Guidelines (WCAG) 2.2, 2023. Useful criteria: 3.3.8 Accessible Authentication (Minimum), 2.2.1 Timing Adjustable.