Recovery codes are often presented as a convenience feature: save these codes somewhere, then use one if you lose access to your normal authenticator. That description can hide their real security role. A recovery code may be enough to regain control of an account, so anyone who obtains a valid code may gain the same recovery path as the legitimate user.

The practical consequence is simple: a recovery code is an authenticator, not a harmless backup string. Its generation, storage, verification, use, replacement, and revocation all belong inside the authentication threat model.

This article explains how to reason about saved recovery codes, why single-use semantics matter, how to store and verify them without keeping recoverable copies, and what risks remain even when the mechanism is implemented correctly.

Start with the authority the code grants

Suppose an account normally requires a password and a second factor. The user loses the second factor but has a saved recovery code.

A simplified recovery flow is:

user cannot use normal authenticator
              |
              v
submit saved recovery code
              |
              v
server verifies code
              |
              v
allow recovery action

The important question is not what the code looks like. It is what successful verification permits.

If the code lets the user register a new authenticator, reset authentication state, or otherwise regain account access, possession of that code carries meaningful authority. Treating it as low-value data because it is rarely used reverses the risk: rare use often means teams exercise and monitor the path less frequently even though the path can bypass a missing primary authenticator.

A useful mental model is to treat each saved recovery code as a spare key with a deliberately narrow job. The analogy stops there. In the actual system, the code is a secret value that the server verifies, and its security depends on unpredictability, protected storage, controlled verification, and lifecycle rules.

State the threat model

Saved recovery codes are intended to help a legitimate user recover when a normal authenticator is unavailable. The design should reduce several related risks:

  • an attacker guessing a valid code online;
  • an attacker who obtains the verifier database learning usable recovery codes from it;
  • a previously used code being accepted again;
  • an old set remaining valid after the user intentionally replaces it;
  • a recovery endpoint becoming an easier authentication path because it lacks the controls applied to normal sign-in.

The mechanism does not protect a code after an attacker steals the user’s copy. A bearer-style recovery code proves possession of the code, not the identity of the person presenting it. It is also not phishing-resistant: a user can disclose it to a convincing fake service.

Recovery codes therefore reduce the availability problem created by a lost authenticator, but they introduce another credential that must remain secret until use.

Generate recovery codes as credentials

A recovery code should come from a cryptographically secure random generator rather than from user information, timestamps, counters, or predictable identifiers. The goal is that knowing one user’s code, or several previously issued codes, does not make another valid code meaningfully easier to predict.

The required size depends on how the code is used and on the surrounding controls. A high-entropy saved recovery code can be designed to resist guessing without relying on memorability. Current NIST digital identity guidance, for example, specifies at least 64 bits of random input for a saved recovery code. That is a useful reference point for systems following that guidance, but an application’s exact design still needs to account for its own risk and interoperability requirements.

Do not reduce randomness merely to make codes look familiar. Recovery codes are normally stored by the user rather than memorized. Human-friendly formatting can improve transcription without reducing the underlying random value: for example, the presentation layer can group characters while verification normalizes only the separators that the format explicitly permits.

The distinction matters. Formatting is a usability decision; entropy is a security property.

Store a verifier, not a usable copy

If the server stores recovery codes in plaintext, a database disclosure can expose credentials that may still be valid. The safer pattern is to store a one-way verifier and compare a submitted code against that verifier.

Conceptually:

issuance:
random recovery code -> one-way derivation -> stored verifier
         |
         +-> shown to user through protected session

verification:
submitted code -> same derivation -> compare with stored verifier

The application does not need to display the original code again. If the user loses it, issue replacement recovery material through an appropriately authenticated flow instead of retrieving the old secret.

The exact derivation depends on the code’s strength. For secrets with enough random entropy, a cryptographic hash can provide an appropriate one-way representation under the system’s assumptions. Shorter lookup-style secrets need stronger protection against offline guessing, such as salted password hashing, because an attacker with the verifier database may be able to enumerate the small input space. Do not assume that hashing alone makes a weak code strong.

This is a useful general rule: storage protection and credential strength solve different problems. A strong random code limits guessing; one-way storage limits what a database disclosure directly reveals.

Make successful use consume the code

A recovery code that remains valid after successful use becomes a reusable credential. That increases the damage from unnoticed copying and makes it harder for a user to know whether an old saved code still grants access.

For a set of recovery codes, track each code independently:

code A -> unused
code B -> used
code C -> unused

After code B successfully authorizes recovery, code B must no longer be accepted. The state transition should be enforced by the authoritative verifier, not only by the user interface.

The check and the consumption also need to behave as one security decision. Two concurrent requests must not both observe the same code as unused and both complete recovery. In a database-backed design, this normally means using a conditional update, transaction, uniqueness rule, or another atomic mechanism appropriate to the data store so that only one request can change the code from unused to used.

This is not merely bookkeeping. Single-use semantics reduce the useful lifetime of a copied code after legitimate use.

Keep verification bounded

Even a well-generated code should not be exposed to unlimited online guesses. Apply rate limiting to the recovery verification path, with controls scoped so that an attacker cannot cheaply bypass them by changing only one request attribute.

The right thresholds depend on code strength, account sensitivity, expected recovery behavior, and denial-of-service risk. A hard lockout after a small number of failures can itself become an attack surface if anyone can intentionally lock another user’s recovery path.

The important design property is bounded verification: repeated failures become progressively constrained or otherwise limited, while legitimate users retain a workable recovery route.

Monitor recovery failures and successes as security-relevant events, but do not put submitted recovery codes into logs, traces, analytics, exception messages, or support tooling. Observability should record facts about the event without copying the credential.

Treat issuance and replacement as sensitive operations

The first set of codes must reach the legitimate user through a protected, appropriately authenticated session. If an attacker can request or view fresh recovery codes after obtaining only a weak session, the strength of the codes themselves does not help.

Replacement deserves the same care. When a user generates a new set, decide explicitly what happens to the previous set. For most saved-code designs, replacing the set should invalidate the old set so that forgotten copies do not silently remain valid.

A useful state transition is:

old set active
      |
user requests replacement
      |
      v
new set issued + old set invalidated

Perform those changes consistently. Avoid a sequence where the new set becomes usable but the old set remains valid indefinitely because a later cleanup step failed.

Recovery-code management should also be visible to the account owner. Notifications for recovery and replacement events can help users detect unexpected activity, although notification is a detection control rather than proof that the event was legitimate.

Decide what recovery is allowed to change

A valid recovery code should grant only the authority the recovery design intends.

For example, a product might use a code to let the user bind a replacement second factor. That does not automatically mean the same code should permit changing the account’s primary email address, deleting audit history, or performing unrelated high-impact actions.

Keep the recovery boundary narrow:

valid recovery code
       |
       +-> recover intended authenticator
       |
       X-> unrelated privileged actions

After recovery, establish normal authentication state deliberately. Depending on the application, that can include revoking sessions that may belong to an attacker, replacing compromised authenticators, and requiring normal authorization for subsequent sensitive actions.

The recovery code answers one question: whether the caller possesses valid recovery material. It does not prove that every other part of the account is trustworthy.

Avoid recovery paths that are weaker than the account

A common design mistake is adding strong authentication to normal sign-in while leaving account recovery dependent on a much weaker path. The effective security of the account can then be dominated by recovery.

This does not mean every application needs the same recovery ceremony. A low-risk service may reasonably choose a simpler mechanism because account loss has limited impact. A system protecting sensitive data or valuable privileges may justify stronger combinations, additional verification, delayed high-impact changes, or more explicit incident handling.

The decision should be made from the authority recovered, not from the rarity of the feature.

Also consider usability as part of the security design. Users need clear instructions to save recovery codes somewhere they can access when their normal authenticator is unavailable. Storing the only recovery copy on the same device as the authenticator can defeat that availability goal. At the same time, scattering copies across email, chat, screenshots, and support tickets increases disclosure risk. The interface should explain both concerns without prescribing one storage method for every user’s environment.

Verify the complete lifecycle

Testing only whether a correct code works misses the security properties that matter most. Exercise the lifecycle around it.

Confirm that a newly issued code succeeds through the intended recovery path, an incorrect code fails without revealing sensitive state, and a successfully used code fails on every later attempt. Send concurrent requests with the same valid code and verify that at most one can consume it successfully. Replace the recovery set and verify that every code from the previous set is rejected.

Then inspect operational paths. Confirm that submitted codes do not appear in logs or traces, rate limiting applies to the real recovery endpoint, recovery events generate the intended notifications, and alternate API or mobile routes cannot bypass the same lifecycle rules.

These tests turn design assumptions into observable behavior.

Understand the residual risk

Recovery codes trade one risk for another. They make loss of a primary authenticator less likely to become permanent account loss, but they create additional secrets that can be copied, phished, misplaced, or stolen.

Strong randomness, one-way verifier storage, single-use consumption, bounded guessing, deliberate replacement, narrow authority, and useful event visibility reduce those risks. They do not make a stolen unused code harmless.

For higher-risk accounts, recovery codes may be one part of a broader recovery design rather than the only proof required. The appropriate combination depends on the account’s value, the consequences of takeover, and which independent recovery channels are actually trustworthy.

Conclusion

Treat saved recovery codes with the same seriousness as other authentication credentials. Generate them unpredictably, deliver them through a protected authenticated flow, store only one-way verifiers, limit guessing, consume each code after successful use, invalidate superseded sets, and keep their authority focused on recovery.

The central design question is not “do we have backup codes?” It is “what authority does possession of this code grant, and have we protected that authority throughout its lifecycle?” Answer that clearly, and recovery becomes a deliberate security boundary instead of an accidental bypass around stronger authentication.