Strong authentication creates a recovery problem: what happens when the user loses the device, key, or application that normally proves who they are? If recovery is much weaker than normal sign-in, an attacker can ignore the strong authenticator and target the fallback instead. If recovery is too difficult, legitimate users can permanently lose access.
A recovery code is a secret generated in advance and kept by the user for this failure case. It acts as a backup authenticator: possession of the code can restore access when the normal authenticator is unavailable.
The security goal is not merely to generate a random string. A useful design makes the code hard to guess, protects it like a credential, accepts it only under controlled conditions, invalidates it after successful use, and gives the user a clear path to restore normal authentication. This article explains that lifecycle and the trade-offs behind it.
Treat recovery as another authentication path
A common mental model is that authentication happens at the login screen and recovery happens somewhere else. From an attacker’s perspective, that distinction does not matter.
If either path grants control of the same account, both paths belong to the account’s authentication boundary:
normal path: primary authenticator -----> account
recovery path: recovery code -------------> accountSuppose normal sign-in requires a password plus a phishing-resistant authenticator, but a lost-device flow accepts a reusable short code with weak verification. The effective account protection can become closer to the weaker recovery path whenever an attacker can invoke it.
This is the threat model for recovery codes: reduce the risk that loss of a primary authenticator causes permanent lockout without creating an easy alternate route for account takeover.
Recovery codes do not protect against every account threat. Malware that can read a stored recovery code may steal it. Phishing can capture a code that a user types into a deceptive site. A compromised authenticated session may bypass the need for recovery entirely. Recovery therefore complements primary authentication, session protection, monitoring, and secure authenticator enrollment rather than replacing them.
Make each code a high-entropy secret
A recovery code is valuable because an attacker should not be able to predict a valid value for an account.
For a simplified example, imagine generating a code from a cryptographically secure random source and presenting it as grouped characters:
7K4M-9Q2R-X8DP-V6TNThe formatting is for readability. The security comes from the unpredictable random value behind it, not from hyphens, mixed case, or visual complexity.
Do not generate recovery codes from usernames, timestamps, counters, ordinary pseudo-random functions intended for simulation, or other predictable account data. Production systems should use the platform’s cryptographically secure random generator and choose an encoding that preserves enough random bits while remaining practical for users to store and enter.
There is no universal code length that fits every system. Shorter codes are easier to type but provide less resistance to guessing and offline recovery of a stolen verifier database. Longer codes improve that margin but can hurt usability. The right choice depends on the generation method, verification rate limits, storage design, and account risk.
The important design rule is to reason about unpredictable entropy rather than the visible number of character classes.
Store a verifier, not the usable code
The server needs to determine whether a submitted recovery code is valid, but it usually does not need to recover the original code after issuance.
That allows the same useful separation used for many authentication secrets:
user keeps: recovery code
server keeps: one-way verifier of recovery codeIf the authentication database is exposed, storing only a suitable one-way representation reduces the chance that the database directly reveals every usable recovery credential.
For recovery codes with limited entropy, a fast unsalted hash alone can make offline guessing unnecessarily cheap. Use a storage construction appropriate to the entropy of the generated codes and the authentication guidance your system follows. Salted password-hashing schemes are appropriate when codes are short enough that offline enumeration is realistic; sufficiently high-entropy generated secrets can use a one-way representation designed for that threat model.
Whatever representation you choose, compare submitted values using the authentication library or primitive intended for that verifier format. Do not log the plaintext code during issuance or verification, and do not place it in URLs where it can leak through browser history, referrers, proxies, or access logs.
Make successful use consume the code
A backup code should normally be a one-time credential. Once a code successfully restores access, accepting the same value again gives anyone who copied it another opportunity to enter the account.
The state transition should therefore be atomic:
unused -> verify successfully -> used“Atomic” matters because two near-simultaneous requests must not both observe the code as unused and both succeed. The exact implementation depends on the datastore, but verification and consumption need transaction or conditional-update semantics that permit only one successful transition.
A simplified flow is:
find unused verifier for account
verify submitted code
mark that verifier used in the same protected operation
continue recoveryIn production, avoid designs that fetch an unused flag, release the database operation, and mark it used later. That gap can turn a nominally one-time code into a code that succeeds more than once under concurrency.
After successful use, either issue a replacement recovery code through a strongly authenticated recovery completion flow or require the user to regenerate their backup set after restoring normal authenticators. The key property is that the consumed value never becomes valid again.
Decide what a successful code is allowed to do
Possessing a recovery code proves possession of that backup secret. It does not automatically prove that every sensitive account action should be available immediately.
For a low-risk service, a valid code may reasonably establish an authenticated session and guide the user to enroll a replacement authenticator. For a high-impact account, recovery can justify additional safeguards before especially sensitive changes, such as changing payment destinations or removing all other authenticators.
The distinction is useful:
recovery code proves: possession of the backup credential
recovery code does not prove: device health, user intent, or absence of theftDesign the post-recovery permissions according to the application’s threat model. A temporary restriction on unusually sensitive actions can reduce damage from a stolen recovery code, but restrictions that are too broad may make legitimate recovery unusable.
Avoid asking for the unavailable authenticator again as the only way to finish recovery. The whole purpose of the path is to handle its loss. Instead, make the recovery credential strong enough for its intended role and apply additional controls only where they address a concrete risk.
Protect code issuance and regeneration
Generating new recovery codes is itself a security-sensitive operation. If an attacker with a stolen session can silently create a new backup credential, the recovery feature can become a persistence mechanism.
Require sufficiently recent and appropriate authentication before displaying or regenerating recovery codes. The exact requirement should match the account’s sensitivity and the strength of the existing session.
Regeneration also needs clear invalidation semantics. If the user asks for a new set, decide whether all previous unused codes become invalid immediately. For most designs, replacing the set is easier to reason about than maintaining several generations of valid backup credentials.
Notify the user through an established channel when recovery credentials are regenerated or used, especially for accounts where takeover would have meaningful impact. A notification is a detection and response aid, not proof that the action was legitimate. It helps a user notice unexpected recovery activity and start remediation sooner.
Help users store the codes separately
A recovery credential only helps when it survives the failure that made the primary authenticator unavailable.
If a user keeps the only recovery code exclusively on the same phone as the authenticator, losing that phone can remove both paths at once. Encourage storage that is both protected and independent of the primary device. Depending on the user’s environment, that might be a securely stored printed copy or a trusted password manager available through an independent recovery path.
“Independent” is the important property. Copying the code into several unprotected notes or sending it through ordinary chat creates more places from which it can leak without necessarily improving recoverability.
The application should display storage guidance when codes are issued, because users need to make this decision before the failure occurs.
Rate-limit verification without relying on it alone
Online throttling makes repeated guessing more expensive and is particularly important when recovery codes have less entropy than long random machine credentials.
Apply limits to the recovery verification path using signals appropriate to the application, such as the account and broader request context. A limit based only on source IP address can be bypassed by distributed traffic and can also block many legitimate users behind shared networks.
Rate limiting does not compensate for predictable codes. It also does not protect a code copied from the user. Think of it as defense in depth around a credential that should already be generated and stored correctly.
Monitor repeated failures and successful recovery events, but keep the actual code out of telemetry. Security logs should record enough context to investigate the event without turning the logging system into a repository of authentication secrets.
Test the failure cases, not only the happy path
A recovery implementation can appear correct in a normal browser test while failing at the boundaries that matter most.
Verify at least these behaviors during security testing:
- an unused valid code succeeds under the intended conditions;
- the same code fails after successful use;
- concurrent attempts cannot successfully consume one code twice;
- regenerating codes invalidates the previous set according to policy;
- invalid attempts are throttled as designed;
- codes do not appear in application logs, analytics, error reports, or URLs;
- recovery events produce the intended notifications and audit records;
- losing the primary authenticator still leaves a documented, usable recovery path.
These tests check both sides of the design: attackers should not gain an easy alternate authentication route, and legitimate users should not discover during an emergency that the fallback never worked.
Understand when recovery codes are not enough
Recovery codes work well when users can securely retain a backup secret and the service can tolerate possession of that secret as a recovery factor. They are inexpensive to operate and do not depend on a live messaging channel at the moment of recovery.
They are less suitable as the only recovery mechanism when users are unlikely to store them reliably, when account consequences are unusually high, or when organizational processes require stronger proof before restoring access. In those cases, additional registered authenticators, carefully designed administrative recovery, or identity re-verification may be justified.
Every extra recovery method adds another path that must be protected and maintained. More options improve availability only if each option has a threat model appropriate to the account.
Keep the fallback aligned with the account’s risk
Recovery codes solve a real availability problem by giving users a credential that survives loss of the primary authenticator. Their security comes from treating that credential as part of authentication rather than as an informal exception.
Generate codes unpredictably, store only appropriate one-way verifiers, consume successful codes exactly once, protect regeneration, keep plaintext values out of logs and URLs, throttle verification, and help users store the backup independently. Then decide what recovery should authorize based on the consequences of account takeover.
The practical test is simple: losing an authenticator should be recoverable, but invoking recovery should not quietly reduce the account to a much weaker security model.