MFA Implementation skill for Claude Code
Second factors with no back door left open. Claude builds MFA as a server-enforced state machine rather than a login screen: a pending-MFA state that reaches nothing else, TOTP with encrypted secrets and replay guards, 64-bit recovery codes, attempt limits, step-up for sensitive actions, and remembered devices that never skip step-up. It also closes the usual bypasses through password reset, token endpoints, support tools and recovery.
- name
mfa-implementation - size279 lines · ≈3,250 tokens
- files1
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Implements multi-factor authentication end to end - factor policy (passkeys, TOTP, push, SMS) mapped to NIST SP 800-63B-4, TOTP enrollment per RFC 6238 with encrypted secrets, replay-safe verification and clock-skew windows, recovery codes, attempt limits, the pending-MFA session state, step-up for sensitive actions, remembered devices, MFA enforcement rollout, account recovery that does not bypass MFA, and audited admin resets. Use when adding 2FA or MFA to login, requiring MFA for admins or an org, building recovery codes or step-up auth, or reviewing an MFA flow for bypasses. Try asking
- Add TOTP two-factor auth with recovery codes to our Django login
- Require MFA for all admins and let org owners enforce it for their members
- Audit our 2FA flow for ways to get in without the second factor
Use it when
- Adding 2FA or MFA to an existing password or SSO login
- Designing recovery codes, lost-device recovery or admin MFA resets
- Requiring fresh verification before payouts, email changes or key exports
- Rolling out mandatory MFA to staff, admins or customer orgs
- Reviewing an MFA implementation for bypass paths
Skip it when
- You are implementing the WebAuthn registration and sign-in ceremonies themselves - use passkeys-webauthn instead
- You need password hashing, cookies and session basics - use auth-sessions instead
How to check the result
Automated tests show an MFA-pending session and password-only token requests are rejected, reused TOTP codes and recovery codes fail, and password reset leaves factors in place.
The common mistake it prevents
Enforcing MFA in the web login controller while the mobile token endpoint or password reset link still issues a full session after the password alone.
Done well means: the second factor is checked on the server before any authenticated resource is reachable, every factor is rate limited and replay-safe, sensitive actions demand a fresh verification, and no path (password reset, support ticket, remembered device, admin tool) silently removes MFA. The weakest enrolled recovery path sets the real strength of the account.
Hard rules
- An account in “password ok, MFA pending” state can call only the MFA verify, factor selection, and logout endpoints. Enforce this in middleware, not in the UI.
- Rotate the session ID when MFA succeeds and record
mfa_verified_atand the factor type (amr) in the session. - Adding, removing or resetting a factor requires a recent MFA verification (step-up) and notifies the user through every channel on file.
- Password reset never disables MFA. Losing all factors goes through the recovery flow.
- Compare codes in constant time; accept each OTP once.
- Use the libraries already in the stack for primitives (
pyotp,otplib, platform WebAuthn libraries); write the policy and state machine yourself.
Factor policy
Rank and present factors in this order. NIST SP 800-63B-4 (final, July 2025) requires verifiers at AAL2 to offer at least one phishing-resistant option.
| Rank | Factor | Phishing-resistant | Policy |
|---|---|---|---|
| 1 | Passkeys / security keys (WebAuthn) | Yes | Default offer; ceremonies per passkeys-webauthn |
| 2 | TOTP app (RFC 6238) | No | Standard fallback; everyone can enroll |
| 3 | Push approval in your own app | No | Only with number matching (user types the number shown at login); “tap to approve” alone is no longer acceptable under 800-63B-4; cap prompts per login |
| 4 | SMS / voice OTP | No | NIST “restricted” authenticator: offer only with an unrestricted alternative, warn the user, check SIM-swap/port signals where available; never the only factor for admins |
| - | Email OTP | No | Not an MFA factor (NIST prohibits email for out-of-band authentication); fine for email verification |
| - | Recovery codes | No | Recovery only, never a daily factor |
Admins, support staff and anyone with production access: phishing-resistant factor required, not offered.
Workflow
- [ ] 1. Inventory: login flow, session store, existing factors, admin tooling
- [ ] 2. Schema: factors, recovery codes, remembered devices, audit events
- [ ] 3. Login state machine with the pending-MFA state
- [ ] 4. TOTP enrollment and verification
- [ ] 5. Recovery codes
- [ ] 6. Attempt limits and lockout
- [ ] 7. Step-up for sensitive actions; remembered devices
- [ ] 8. Recovery and admin reset flows
- [ ] 9. Enforcement rollout (optional -> nudged -> required)
- [ ] 10. Bypass tests
2. Schema
create table mfa_factors (
id uuid primary key,
user_id uuid not null references users(id),
kind text not null check (kind in ('webauthn','totp','sms','push')),
label text not null, -- "Pixel 9", "YubiKey 5C"
secret_ciphertext bytea, -- TOTP only: encrypted with KMS/envelope key, never hashed
last_used_step bigint, -- TOTP replay guard
phone_e164 text, -- SMS only
confirmed_at timestamptz, -- null = enrollment not finished, not usable
created_at timestamptz not null default now(),
last_used_at timestamptz
);
create table mfa_recovery_codes (
user_id uuid not null references users(id),
code_hash text not null, -- HMAC-SHA256 with a server pepper, or argon2id
used_at timestamptz,
primary key (user_id, code_hash)
);
create table remembered_devices (
id uuid primary key, user_id uuid not null, token_hash text not null unique,
user_agent text, created_at timestamptz not null, expires_at timestamptz not null,
revoked_at timestamptz
);
Audit every event: enroll, confirm, remove, verify success/failure, recovery code used, codes regenerated, device remembered/revoked, admin reset. Include actor, IP, user agent.
3. Login state machine
anonymous --password ok, MFA enrolled--> mfa_pending (TTL 10 min, max 5 attempts)
mfa_pending --factor verified--> authenticated (new session ID, mfa_verified_at=now)
mfa_pending --TTL or attempts exceeded--> anonymous (restart from password)
anonymous --password ok, remembered device valid--> authenticated (amr: pwd + device)
Never put “MFA passed” in a client-readable flag or a JWT claim the client can mint. If
you issue tokens, set amr/acr server-side only after verification.
4. TOTP
Parameters: SHA-1, 6 digits, 30-second step. Other values are allowed by the RFC but many authenticator apps ignore them. Secret: 20 random bytes (160 bits, 32 base32 chars), above NIST’s 112-bit floor.
Enrollment:
- Require step-up (fresh password or existing factor).
- Generate the secret server-side, store encrypted with
confirmed_at = null. - Show a QR code of
otpauth://totp/Example:alice%40example.com?secret=<BASE32>&issuer=Exampleand the secret as text for manual entry. Render the QR server-side or in the page; never send the secret to a third-party QR service. - Activate only after the user enters a valid current code. Then show recovery codes.
- Notify the user that a new factor was added.
Verification with replay protection (Python, pyotp):
import hmac, time, pyotp
STEP = 30
def verify_totp(factor, code: str, now: float | None = None) -> bool:
if not (code.isdigit() and len(code) == 6):
return False
now = now or time.time()
totp = pyotp.TOTP(decrypt(factor.secret_ciphertext))
current = int(now // STEP)
for step in (current - 1, current, current + 1): # +/-1 step skew window
if hmac.compare_digest(totp.at(step * STEP), code):
if factor.last_used_step is not None and step <= factor.last_used_step:
return False # reused or older code
# conditional update so two concurrent requests cannot both succeed
return mark_step_used(factor.id, step) # UPDATE ... WHERE last_used_step IS NULL OR last_used_step < :step
return False
Node with otplib v13 (functional API; confirm option names for your version):
import { verify } from "otplib";
const result = await verify({
secret,
token: code,
epochTolerance: 30, // seconds either side, i.e. +/-1 step
afterTimeStep: factor.lastUsedStep ?? undefined, // rejects reused steps
});
if (result.valid) await saveLastUsedStep(factor.id, result.timeStep);
Skew: allow at most one step either side. Wider windows multiply guessable codes. If users report “codes never work”, check server NTP before widening anything.
5. Recovery codes
- 10 codes, each at least 64 bits of entropy (NIST 800-63B-4 for saved recovery codes):
16 base32 characters shown as
ABCD-EFGH-JKLM-NPQR. - Store only hashes; mark used rather than delete, so the audit trail shows which one.
- Show once at enrollment with download/copy; regenerating invalidates the old set and requires step-up plus a notification.
- Using a recovery code logs the user in, then prompts them to enroll a new factor and shows how many codes remain. Rate limit like any other factor.
import secrets, hmac, hashlib, base64
def new_recovery_codes(n=10):
codes = [base64.b32encode(secrets.token_bytes(10)).decode()[:16] for _ in range(n)]
pretty = ["-".join(c[i:i+4] for i in range(0, 16, 4)) for c in codes]
hashes = [hmac.new(PEPPER, c.encode(), hashlib.sha256).hexdigest() for c in codes]
return pretty, hashes # show pretty once, store hashes; normalize input (strip dashes, upper)
6. Attempt limits
| Scope | Limit | On exceed |
|---|---|---|
| Per pending-MFA session | 5 wrong codes | End session, restart from password |
| Per account, per factor | 10 failures in 15 minutes | Temporarily disable that factor, notify user |
| Per account, consecutive | Well under NIST’s ceiling of 100 consecutive failures per authenticator | Disable factor until recovery |
| SMS sends | 3 per 15 minutes per account, plus per-number and per-IP caps | Block sends; SMS pumping costs money |
| Push prompts | 3 per login attempt | Require another factor |
A new code sent by SMS does not reset the failure counter.
7. Step-up and remembered devices
Step-up: sensitive actions (change email, password or factors, view API keys, payouts,
export data, delete account, admin impersonation) require mfa_verified_at within the
last 5 to 15 minutes; otherwise return a step_up_required error that the client turns
into a verification prompt, then retries the action. Bind the step-up to the session, not
to a long-lived token.
Remembered devices (“trust this browser for 30 days”):
- 32 random bytes in a
__Host-cookie (Secure,HttpOnly,SameSite=Lax); store the hash with user ID and expiry; maximum 30 days. - Skips the second factor at login only. Never skips step-up.
- Revoked on password change, factor reset, account recovery, and “sign out everywhere”; listed in security settings with revoke buttons.
- Not offered to admins or under org policies that forbid it.
Session timeouts for reference: 800-63B-4 sets AAL2 reauthentication at most 24 hours overall and 1 hour of inactivity; AAL3 at most 12 hours overall and 15 minutes inactivity.
8. Recovery and admin reset
User lost every factor and every recovery code:
- Recovery request from the login page; never from inside a session that is MFA pending.
- Verify identity beyond email: for consumer apps, email link plus a waiting period (for example 24 to 72 hours) with notifications to every channel and a one-click “this was not me” cancel; for business accounts, the org admin resets it.
- On completion: remove old factors, revoke all sessions and remembered devices, force new enrollment before any other action, notify.
Admin or support reset:
- Org admins reset members of their own org only; nobody resets their own MFA through the admin tool.
- Support resets need a ticket reference and identity check recorded in the audit log; for high-value accounts require a second approver.
- Support tools never display TOTP secrets or recovery codes.
- Every reset notifies the user and appears in the org’s audit log.
9. Enforcement rollout
- Optional, with a settings page and enrollment nudge after login.
- Required for staff and admin roles immediately.
- Org-level “require MFA” setting with a grace period (for example 14 days) showing a countdown; after it, users must enroll at next login before reaching the app.
- Measure enrollment rate and lockout/support volume per week before tightening.
Gotchas
- MFA checked in the login controller, but the API or mobile token endpoint issues tokens after password alone.
- “Remember me” session cookie created before MFA completes.
- OAuth/SAML login paths skip MFA for local accounts linked to an external identity;
decide explicitly whether the IdP’s MFA (
amr/acr) satisfies yours. - TOTP secret hashed instead of encrypted: verification becomes impossible.
- The same TOTP code accepted twice within its window because
last_used_stepwas not updated atomically. - Factor removal endpoint that accepts only a session cookie: a stolen session removes MFA. Require step-up.
- Error messages that reveal whether the password was right (“enter your 2FA code”) to unauthenticated callers on a credential-stuffing path. Rate limit the password step too.
- Password reset email that logs the user in directly, skipping MFA.
Verification
Write automated tests for each bypass attempt and run them on every change:
- Protected API called with an
mfa_pendingsession returns 401/403. - Token, mobile and API-key endpoints cannot be reached with password only.
- Same TOTP code twice: second fails. Code from two steps ago fails.
- Sixth wrong code in a pending session ends it.
- Recovery code works once; reused code fails; regenerate invalidates the old set.
- Password reset leaves factors in place and still asks for MFA.
- Remembered device cookie stops working after password change; it never satisfies step-up.
- Factor removal without fresh verification is rejected.
- Admin reset of a user in another org is rejected and every reset produces an audit event and notification.
Output contract
## MFA: <app>
- Factors offered: passkeys, TOTP (SHA-1/6/30, +/-1 step), SMS <yes/no, restrictions>
- Required for: <roles/orgs>; enforcement: <optional | grace period N days | required>
- Pending-MFA state: TTL <n> min, <n> attempts; endpoints allowed: <list>
- Recovery codes: <n> x <bits> bits, hashed with <method>
- Step-up window: <n> min for <actions>
- Remembered devices: <n> days, revoked on <events>
- Recovery: <flow, waiting period>; admin reset: <who, approvals, audit>
- Bypass tests: <n>/<n> passing About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.