Test the full code lifecycle
Use a controllable clock in staging. Submit a code one moment before and one moment after expiry. Request a resend, then submit both the old and new code. Submit a valid code twice at the same time to test single use. Change the pending action amount before submitting the code. Save attempt counts across resend and process restart. The result should match the documented validity rule without opening extra guesses.
Test cases and proof
| Case | Expected result | Proof to keep |
|---|---|---|
| Old code after resend | Follow one clear validity rule | Accept or reject trace |
| Code after transfer amount changes | Reject for changed action | Transaction state |
| Repeated guesses across resend | Keep attempt limit | Counter log |
Synthetic example
Request code 111111, then request a resend that creates 222222 in a test harness. Try both at expiry boundaries and after changing the transfer amount. Document exactly which code remains valid and how the guess counter persists.
Evidence to keep
Record every code issue, resend, failed submit, accepted submit, and expiry event under one test action ID. Use distinct fake codes in the harness and never place real OTP values in a shared report. The evidence should show a single accepted business action and a guess counter that does not reset through resends.
Test code entry from the wrong device if the product binds codes to a session. Also test a code requested for sign-in against a transfer approval. A code valid for one purpose must not complete another. Keep purpose and action ID in the test trace.
Resend should not buy more guesses
A customer may resend because a message is late. An attacker may resend to reset an attempt counter. Keep the counter with the account and pending action, not with one message ID alone. Decide whether the old code remains valid after resend and show that rule in the product. Test a code at expiry boundaries and after its first accepted use. If two submissions race, only one business action should complete. Give the customer a recovery path when delivery fails without making codes valid without limit.
Related reading
Why these checks matter
NIST separates authenticator-generated OTPs from out-of-band secrets and covers one-time use, while OWASP’s authentication guide covers attempt controls and account recovery. The resend rule is a product decision, but the test must show that resending cannot create more guesses, extend a code without bound, or approve a different action.
Name the kind of code
This resend matrix covers a server-issued challenge delivered to a channel, such as SMS. An authenticator-generated TOTP changes on its own clock and has no server “resend new code” flow. Email verification and email recovery codes have their own purpose and policy; do not call them NIST-compliant out-of-band authentication merely because they contain six digits.
For the synthetic transfer challenge, set expiry to two minutes, replace the old code on resend, and keep a five-failure account budget. These are fixture settings. Issue C1, make four failed attempts, resend C2, and make one more failed attempt. A sixth verification attempt is limited even if C2 was just issued. Sending new messages must not reset verification failures.
Use a fresh fixture for each destructive check
Expiry and single-use tests consume or invalidate state. Create independent challenge IDs for each test: one just before expiry, one at the stated expiry boundary, one after expiry, one after resend, and one for concurrent submission. If policy binds the challenge to the initiating session, submission on a second device must fail; otherwise it may succeed once. Choose that rule before checking reuse on the first device.
Run sign-in and transfer challenges against each other to check purpose binding. Reject a valid code when the transfer version changed. Fake fixed codes belong only in a deterministic test harness; live codes need secure randomness and no plaintext logging. Save challenge ID, purpose, time, counter, action version, and outcome. NIST’s out-of-band verifier section says a new secret must not reset the failed-attempt count.