Adding a second factor is a good afternoon's work. Deciding what happens when somebody loses it is a design problem, and the answer determines whether the second factor means anything at all.
Every path around the factor is equivalent to the factor. If a lost phone can be resolved by clicking a link in email, then the factor is email, and the hardware key the customer was so pleased about is decoration.
What a code has to be
Recovery codes are credentials of the same strength as the thing they replace, which means they are generated from a cryptographic random source, long enough that guessing is hopeless, stored hashed, and consumed on first use. Storing them in plain text so support can read one out defeats the entire mechanism, and it will happen if you leave the possibility open.
Issue a set at enrolment, show them exactly once, and make downloading or printing them the obvious action rather than a link in small type. Regenerating a set invalidates every previous code — partially valid sets are how somebody ends up with an old printout that still works two years after they thought they revoked it.
- Generated from a cryptographic source, not a convenience random function
- Hashed at rest; no path in your product ever displays an existing code
- Single use, and consumed atomically so two concurrent attempts cannot share one
- Regeneration invalidates the whole previous set
- Rate limit and lock out on repeated failures, exactly as with a password
Every path around the second factor is equivalent to the second factor. Including the one your support team uses.
Tell people when they are running out
A set of ten codes with no visibility into how many remain is a slow-motion lockout. Show the count, warn when it gets low, and prompt to regenerate. Record every use as an audit event, because a recovery code being consumed is a genuinely interesting signal — it is either somebody who lost a device or somebody who should not be there.
Treat a use as a reason to check in. A notification to the account owner on every recovery-code sign-in costs nothing and is the fastest detection you will get for a stolen printout.
The support path is the real policy
Eventually somebody exhausts their codes and contacts support, and whatever your team does next is your actual recovery policy, regardless of what the documentation says. Write it down and constrain it: who may reset a factor, what evidence they require, that it is logged as an administrative action visible to the customer, and that the person is required to re-enrol immediately rather than left without one.
In a product with organisations, the better answer is usually to remove your team from the loop. Let a customer's own administrator reset a member's factor, with the action recorded in their audit log. They know who their people are, they are accountable for the decision, and it removes the most attractive social-engineering target from your support queue. Where the organisation uses enforced single sign-on, the identity provider owns this problem entirely — and saying so plainly is a better answer than building a parallel path around it.
- Administrative reset is a logged, customer-visible event with a named actor
- Prefer customer administrators over your support team as the reset path
- Under enforced SSO, defer recovery to the identity provider rather than duplicating it
- Require immediate re-enrolment; never leave an account with no factor
Everything here, already built
Sign-in, enterprise SSO, directory provisioning, roles and an audit trail behind one API. Start with the quickstart and have a working sign-in this afternoon.