Two-Factor Authentication (OTP)
How 2FA and one-time codes work (SMS, email, authenticator apps, push approval, backup codes), what usually breaks in OTP screens, and how to test codes, resends, lockouts and recovery on mobile and web.
You log in to your banking app, and before you see your balance it asks: “Enter the 6-digit code we sent to •••• 0187”. You switch to Messages, read the code, switch back, type it, and you’re in. That second step is two-factor authenticationTwo-factor authenticationLogging in with two proofs, usually a password plus a one-time code.Open in glossary → (2FA), and the code is a one-time password (OTP).
2FA exists because passwords leak. People reuse them, phishing sites collect them, old databases get stolen. With 2FA, a stolen password alone isn’t enough: the attacker also needs your phone. For testers that makes the OTP screen a small screen with a big job. It must stop attackers, and at the same time let a real person in, fast, while they juggle apps, slow SMS and a code that expires.
How it works
The three kinds of factors
A “factor” is a type of proof. Real 2FA combines two different types. A password plus a security question is still one type (two things you know).
| Factor | Meaning | Examples |
|---|---|---|
| Knowledge | Something you know | Password, PIN |
| Possession | Something you have | Phone (SMS, authenticator app, push), security key |
| Inherence | Something you are | Face ID, fingerprint (see biometric authenticationBiometric authenticationUnlocking a saved login with Face ID, Touch ID or a fingerprint.Open in glossary →) |
The flow, step by step
- First factor. The user enters email and password. The server checks them but does not start a full session yet. It remembers “password OK, waiting for step 2”.
- The server issues a challenge. For SMS or email it generates a random code (usually 6 digits), stores it with an expiry (often 5 to 10 minutes) and sends it. For an authenticator app nothing is sent: the app already knows how to compute the code.
- The user enters the code. They type it, paste it, or accept the phone’s autofill suggestion. Many apps submit automatically once all digits are in.
- The server checks it. Right code, not expired, not used before, and not too many wrong attempts? Then the code is marked as used and a sessionSessionThe period during which a user stays logged in without entering their credentials again.Open in glossary → starts.
- Optional: remember this device. The server gives the device a long-lived token, so this phone or browser skips step 2 for, say, 30 days.
Where the code comes from
| Method | How it works | Watch out for |
|---|---|---|
| SMS | Server texts a random code to the phone number | Slow or missing SMS, roaming, SIM swap attacks, cost per SMS |
| Server emails a code or a link | Spam folder, delays, weaker if the email account uses the same password | |
| Authenticator app (TOTP) | App and server share a secret at setup; both compute a new code every 30 seconds from the secret and the current time | Phone clock out of sync, lost phone, setup via QR code |
| Push approval | The app on a trusted phone shows “Is this you logging in?” with Approve/Deny | Users approving pushes they didn’t start (“push fatigue”), no internet on the phone |
| Backup / recovery codes | A list of single-use codes shown once at setup, to use when the phone is lost | Each code works once, the list can be regenerated (old list must stop working) |
SMS is the most familiar method, but not the strongest. The US NIST digital identity guidelines (SP 800-63B) classify codes sent by SMS or phone call as a restricted method, because of risks like SIM swaps and interception, and expect services to offer users an alternative. That’s why many products also support authenticator apps or security keys.
Authenticator apps and clock skew
TOTPTOTPTime-based one-time password: an authenticator app and the server compute the same code every 30 seconds.Open in glossary → (time-based one-time password, defined in RFC 6238) is what Google Authenticator, Microsoft Authenticator and similar apps use. Nothing is sent over the network: the server and the app both compute the code from the shared secret and the current time, in steps of (by default) 30 seconds. That means the phone’s clock matters. Try it:
Authenticator app
Northbank · leo@test.com
––– –––
Server accepts
Same secret + server time. One step either side is a common tolerance.
previous
––––––
current
––––––
next
––––––
Many servers also accept the code from one step before and after the current one, to cover small clock differences and the seconds it takes to type. If the phone clock is set manually and drifts further, every code is “wrong”. That’s clock skew, and it’s worth a test: set the device time manually a minute or two off and see what the app tells the user.
OTP input: the small details that matter
The code field looks simple, but it’s where most user frustration happens. Everything from Text Field applies, plus:
- One-time code autofill. iOS suggests a code from Messages (or Mail) above the keyboard when the field is marked as a one-time-code field. Android can do the same with the SMS Retriever / User Consent APIs, and Chrome on Android supports the WebOTP API for websites. Test that the suggestion appears and fills the field correctly.
- Paste. People copy codes from SMS, email or a password manager. Pasting
318452,318 452or318452(with spaces) should all work. - Numeric keyboard. The field should open the number pad, but keep the value as text: codes like
042917start with a zero, and a number would lose it. - Auto-submit. Submitting when the last digit is entered is convenient, but it must fire exactly once, and the user must still be able to fix a typo.
- One field or six boxes? Both are common. A single field is simpler and usually works better with autofill, paste and screen readers. Separate boxes look nice but often break paste, backspace, and focus movement. If you test boxes, try paste, backspace across boxes, and tapping a box in the middle.
Example requirements
What usually goes wrong
| # | Bug | What you see | Typical severity |
|---|---|---|---|
| 1 | Code never expires | A code from this morning still works | High (security) |
| 2 | Code replay | The same code logs in twice (two devices, or after logout) | High (security) |
| 3 | Old code works after resend | Spec says resending cancels older codes, but they still work | Medium–High |
| 4 | No lockout | Unlimited guesses: 6 digits are only 1,000,000 combinations | Critical (security) |
| 5 | No resend cooldown | Tapping Resend sends an SMS every time (spam, cost, rate limit by the SMS provider) | Medium–High |
| 6 | Leading zero lost | Codes like 042917 always fail because the app treats the code as a number | High |
| 7 | Paste broken | Pasting 318 452 is cut off or rejected | Medium |
| 8 | Auto-submit twice | Error “code already used” right after a successful login | Medium |
| 9 | Autofill doesn’t work | iOS/Android never suggest the code from the SMS | Medium |
| 10 | 2FA can be bypassed | Going straight to a deep link or a protected URL after step 1 skips the code | Critical (security) |
| 11 | 2FA off without re-auth | Anyone holding an unlocked phone can disable 2FA in Settings | High (security) |
| 12 | No way back in | Lost phone and no backup codes or recovery path, so support is the only option | High |
Find the bug
Northbank’s SMS code screen below has six bugs. Under the phone there’s a Messages panel (the SMS that arrive on the phone), a clock you can move forward, the spec, and a server log. Explore like a tester:
- Log in, read the SMS and enter the code. Use the From Messages suggestion too.
- Tap Resend code a few times quickly. Then try the older codes.
- Copy a code from Messages (it contains a space) and paste it.
- Enter wrong codes many times.
- Watch the server log for every request, and look closely at what’s sent.
🐞 Bug hunt: Northbank SMS code
This demo has 6 hidden bugs. Explore it like a tester, then check your findings.
Northbank
Password is already filled in. Step 2 is the SMS code.
Clock
Messages · Northbank
No messages yet. Tap Log in on the phone.
From the spec
- A code expires after 5 minutes and works once.
- Only the newest code is valid; resending cancels older codes.
- "Resend code" is available 30 seconds after the last SMS.
- After 5 wrong codes, the user must log in again.
- Pasting a code with spaces works; codes can start with 0.
Server log
- Requests appear here…
- 1
Hint: Tap Resend twice, then enter the first code you received.
- 2
Hint: Resend until you get a code that starts with 0. Read the verify request in the server log.
- 3
Hint: Copy a code from Messages (it is shown as "318 452") and paste it.
- 4
Hint: Enter wrong codes six or more times and watch the "failed" count in the log.
- 5
Hint: Tap Resend code five times in a row.
- 6
Hint: Enter a correct code (or tap the From Messages suggestion) and watch what happens after you are logged in.
Test scenarios
Functional
- Correct code logs in; the session starts only after the code (not after the password).
- Each method the product supports: SMS, email, authenticator app, push approval, backup codes.
- Resend sends a new code; the newest code works.
- Autofill: the iOS “From Messages” suggestion and the Android equivalent fill the code; auto-submit logs in once.
- Paste of
318452,318 452and the code with leading or trailing spaces. - Trust this device: the next login on this device skips the code; another device still asks; revoking in Settings makes it ask again.
- Enable 2FA: scan the QR code in an authenticator app, confirm with a code, see and save backup codes.
- Disable 2FA, change phone number, regenerate backup codes: each asks for the password again (or another strong check).
Negative
- Wrong code, empty field, 5 digits, letters.
- Expired code (wait past the limit, or ask for a test environment with a short expiry).
- Used code: log in, log out, log in again with the same code. It must fail (no replay).
- Old code after resend: compare with the spec.
- Too many wrong codes: the lockout kicks in at exactly the agreed number, and resending or restarting the app doesn’t reset the counter.
- Resend too fast: the button is disabled during the cooldown, and the server rejects requests that bypass it.
- Old backup codes after generating a new list.
- Skipping step 2: after a correct password, try opening a protected screen directly (a deep linkDeep linkA link that opens a specific screen inside an app instead of just its home screen.Open in glossary → on mobile, a URL like
/accountson the web). It must ask for the code first.
Edge cases
- Leading zeros: make sure you see at least one code starting with
0(resend a few times, or ask a developer for a fixed test code). - Expiry boundaries: just before and just after the limit (for example 4:50 and 5:10 for a 5-minute code).
- Lockout boundaries: 4 wrong then 1 right (works and resets the counter); exactly 5 wrong (locked).
- Clock skew for authenticator apps: set the phone time manually 30, 60 and 120 seconds off.
- Code arrives late: the SMS for code A arrives after you’ve requested code B.
- Switching apps: leave the app to read the SMS, return, and check the screen, the typed digits and the timer are still there. See Interruptions.
- Phone number formats: international numbers, numbers changed recently, roaming.
- Two devices: start login on the web, receive a push on the phone, and Deny it; the web login must fail.
- Lost phone: log in with a backup code, then use the same backup code again (must fail).
UI & usability
- The screen says where the code was sent (masked number or email) and how long it’s valid.
- Numeric keyboard opens; the field accepts digits only and doesn’t lose leading zeros.
- Resend shows a visible countdown; after resending, the user sees that a new code was sent.
- Errors are specific and helpful: “That code isn’t right” vs “This code has expired, request a new one”.
- There’s a clear link for “I can’t access my phone” (backup code or recovery).
- With separate boxes: paste fills all boxes, backspace moves to the previous box, and focus never gets stuck.
Security basics
Defensive checks you can do without special tools:
- Codes expire, work once, and old codes behave as the spec says after a resend.
- Lockout after the agreed number of wrong codes, enforced on the server. That’s what stops a brute-force attackBrute-force attackGuessing passwords or codes over and over until one works.Open in glossary →.
- Rate limits on sending codes (rate limitingRate limitingCapping how many requests a user or device can make in a period of time.Open in glossary →), so nobody can flood a phone with SMS.
- The code is never in the URL, logs or analytics, and the full phone number isn’t shown on screen.
- The protected area is unreachable until the code is verified.
- Turning 2FA off and changing the phone number require re-authentication and trigger a notification email.
Accessibility
- The code field has a label (“Verification code”), not only a placeholderPlaceholderHint text shown inside an empty input field that disappears when the user starts typing.Open in glossary →. With separate boxes, the screen reader must not announce six unlabeled fields.
- Errors are announced by the screen readerScreen readerSoftware that reads the screen aloud for blind and low-vision users, like VoiceOver or TalkBack.Open in glossary →, and the countdown doesn’t spam announcements every second.
- The time limit is long enough (WCAG asks for a way to extend most time limits): a user who reads slowly or switches apps must still make it, or can easily request a new code.
- Pasting is never blocked; people using password managers and assistive tech rely on it.
Example test cases
TC-2FA-001Log in with a valid SMS codepositivehigh
Preconditions
- 2FA by SMS is enabled for leo@test.com
- User is logged out
Steps
- Log in with the correct email and password
- Open the SMS and enter the 6-digit code
Expected result
The code is submitted once and the Home screen opens. One verify request in the log. No error appears.
TC-2FA-002Code with a leading zero is acceptededgehigh
Preconditions
- Test environment returns the fixed code 042917, or resend until a code starts with 0
Steps
- Log in with the correct password
- Enter 042917
- Inspect the verify request
Expected result
User is logged in. The request sends "042917" as a string.
TC-2FA-003Pasted code with a space is acceptededgemedium
Steps
- Copy "318 452" from the SMS
- Paste it into the code field
Expected result
The field shows 318452 and the code is submitted. No error.
TC-2FA-004Resend cancels the previous codenegativehigh
Steps
- Receive code A
- Wait 30 seconds and tap "Resend code"
- Receive code B
- Enter code A
Expected result
Code A is rejected. Code B logs in.
TC-2FA-005Lockout after 5 wrong codesnegativehigh
Steps
- Enter 5 different wrong codes
- Enter the correct code
- Tap Resend and try a new code
Expected result
After the 5th wrong code the user is sent back to the password step. The correct code no longer works, and resending does not reset the counter.
TC-2FA-006Resend cooldownnegativemedium
Steps
- Request a code
- Try to tap "Resend code" immediately
- Wait 30 seconds and tap again
Expected result
Resend is disabled with a countdown for 30 seconds, then sends exactly one new SMS.
TC-2FA-007Code cannot be reusednegativehigh
Steps
- Log in with a valid code
- Log out
- Log in with the password and enter the same code within 5 minutes
Expected result
The code is rejected ("This code has already been used").
TC-2FA-008Disabling 2FA requires the passwordnegativehigh
Preconditions
- User is logged in with 2FA enabled
Steps
- Open Settings > Security
- Turn off two-factor authentication
Expected result
The app asks for the password (and/or a code) before turning 2FA off, then sends a notification email.
Tip: click a test case to expand it.
Mobile vs web
- SMS autofill: on iOS, the code from Messages appears above the keyboard; on Android, test the autofill or consent prompt your app uses. Check it with the app in the foreground and when the SMS arrives while the keyboard is closed.
- Switching apps: read the code in Messages or an authenticator app and come back. The app must not restart the flow, clear the field, or send a new code. Also test a phone call arriving while you’re on the code screen (Interruptions).
- Push approval: test Approve, Deny, ignoring it, and approving after it expired. Test with notifications turned off for the app: there must be a fallback.
- Authenticator on the same phone: copy the code from the authenticator app and paste it. Set the phone time manually to test clock skew.
- New phone or reinstall: is “trust this device” reset? Are users stuck if the authenticator app wasn’t moved to the new phone? That’s when backup codes and recovery matter.
- Biometrics: some apps let Face ID or fingerprint replace the code on a trusted device. Test the fallback when biometrics fail.
- Autofill: Safari can fill codes from Messages on a Mac or iPhone; Chrome on Android supports WebOTP for SMS codes; password managers can fill TOTP codes.
Dev note
The field needsautocomplete="one-time-code"for most of this to work. - Multiple tabs: start login in two tabs, verify in one. The other tab shouldn’t end up logged in as a different step, or crash.
- Skipping step 2: after the password, type a protected URL directly or use the Back button. You must land on the code screen, not inside the account.
- Network tab: in DevToolsDevToolsDeveloper tools built into browsers for inspecting pages, network traffic and errors.Open in glossary →, check the verify request: the code is sent as a string, in the body of a
POST, and the response doesn’t leak the expected code. - “Trust this browser”: check the cookieCookieA small piece of data a website stores in your browser, for example to keep you logged in.Open in glossary → it sets, test private windows (should ask every time), and clearing cookies (should ask again).
- Keyboard only: Tab to the field, type, submit with Enter, reach the Resend link; focus is visible throughout.
Checklist
2FA / OTP checklist
0 of 15 checked
Check your understanding
Quick quiz
0/6 answered · 0 correct
1.Which pair is real two-factor authentication?
2.Customers say "sometimes my SMS code doesn't work". You notice every failing code starts with 0. What is the likely cause?
3.An authenticator code is always rejected on one phone, but works on others. What should you check first?
4.After a successful login, an alert says "This code has already been used". The log shows two verify requests. What is the bug?
5.Which test best checks protection against guessing codes?
6.In Settings, a logged-in user can turn off 2FA with a single tap. Why is that a problem?
Key takeaways
- 2FA combines two different types of proof (know, have, are). The OTP is the “have” part, so it must be short-lived, single-use and limited in attempts.
- Test the code lifecycle: expiry, reuse, old codes after resend, lockout, and resend cooldown, always against the spec and at the boundaries.
- Treat codes as text: leading zeros, spaces in pasted codes, autofill and paste must all work.
- Test the real-life journey: switching apps to read the SMS, late SMS, wrong phone clock, lost phone and backup codes.
- Enabling, disabling and changing 2FA settings need re-authentication, and nobody should reach the account before the code is verified.
Finished this page?Nice work, this page is complete!
Mark it as done to track your progress. It's saved in this browser, no account needed.
Its review cards are now in Bites ⚡