# Two-factor authentication during sign-in

This guide covers consumer accounts. [Tenant two-factor](https://m7.org/docs/api/sso.user.m7.org/tenant-two-factor.md)
has organization/member ownership, org-only availability switches and different
existing-session/Device Code semantics. Consumer settings are not inherited by
tenant members, including consumer-linked members.

M7 consumer accounts can link an authenticator and choose where a code is
required in [two-factor settings](https://user.m7.org/members/settings/2fa).
Enrollment supports a QR code or manual setup key for a standard TOTP app
using SHA-1, six digits, and a 30-second period.

Applications continue to use [authorization code with PKCE](https://m7.org/docs/api/sso.user.m7.org/quickstart.md)
or [Device authorization](https://m7.org/docs/api/sso.user.m7.org/device-authorization.md). M7 hosts the password,
authenticator, and recovery screens. Your application must not collect an M7
authenticator secret, TOTP code, or recovery code, or submit those values to
the OAuth token endpoint.

## Where checks apply

The user controls four independent settings after linking and verifying an
authenticator:

| Setting | When M7 asks for a code |
| --- | --- |
| Password logins | After a correct M7 password during regular browser authorization, before completing that password sign-in. |
| Email sign-in | After successful magic-link or email-code proof during consumer browser authorization. |
| Device Code | After a correct M7 password during Device Code approval, before completing approval. |
| Account switches and existing-session sign-ins | When selecting a remembered consumer account or using an account already open in the browser to authorize an application, subject to the selected interval. |

These settings apply to consumer accounts. The password and Device Code
settings do not depend on each other. Email sign-in has its own setting,
which defaults to off and applies to both email methods. See
[email sign-in](https://m7.org/docs/api/sso.user.m7.org/email-sign-in.md) for browser binding, confirmation, and expiry.

A fresh federated sign-in uses the provider's authentication policy and does not add the local-password TOTP
step. Later reuse of that remembered account follows M7's account/session
setting, even when the account originally signed in through federation.

Refresh-token requests and machine `client_credentials` issuance do not prompt
for these codes. Refreshing a token does not restart the account-verification
freshness interval. Linking an authenticator, or enabling one setting, does
not mean every login path is subject to the same check.

## Account switches and existing sessions

The interval choices are **Every time**, **15 minutes**, **one hour**, and
**eight hours**. A missing or expired verification requires a new code. Within
a nonzero interval, recent successful verification for the same account in
the same browser session can satisfy the check. Another account, another
browser, or a replacement authenticator does not inherit that proof.

Every time means one proof for each new protected authorization transaction,
not another prompt on every page within the same transaction. Selecting an
account and continuing its authorization should not require duplicate codes.
Using an existing M7 login to enter a different application is also an
authorization transaction; it is not a refresh-token request.

The verification interval is an M7 account setting. It is separate from the
OIDC `max_age` parameter and does not create a new token lifetime or refresh
policy.

## What an application receives

On success, the normal authorization callback and code exchange remain
unchanged. Validate `state`, use the transaction's PKCE verifier, and follow
the existing [callback contract](https://m7.org/docs/api/sso.user.m7.org/authorization-callback-outcomes.md).
Do not infer an `amr` or `acr` guarantee from the fact that an M7 account has
enabled one of these settings.

For `prompt=none`, an existing session can still require a code. When that
account-verification step is due and callback trust has been established,
M7 returns `error=login_required` with
`error_reason=account_two_factor_required` to the registered callback. Validate
state and start a new interactive authorization without `prompt=none` when
the user chooses to continue. Do not repeatedly retry the silent request.

During Device Code approval, token pickup remains pending until the required
sign-in, two-factor proof, and approval complete. Continue respecting the
returned polling interval and the existing `authorization_pending`,
`slow_down`, denial, and expiry behavior. The application never receives the
user's TOTP or recovery code.

An invalid or expired code does not complete sign-in. M7 applies proof-attempt
limits and may require restarting the flow. A successful TOTP time step is
not reusable; wait for the app's next code before another successful proof.

## Recovery codes

After linking an authenticator, the user can generate ten recovery codes in
M7 settings by confirming with a current authenticator code. The codes are
shown once. Generating another set invalidates the entire previous set.

On a hosted two-factor prompt, **Use a recovery code** accepts one unused
code in place of the authenticator code. It remains part of the ongoing
sign-in: it is not a standalone password replacement or an OAuth credential.

M7's recovery screen checks **Disable two-factor authentication** by default:

- With that option selected, successful recovery fully unlinks the
  authenticator, turns all four checks off, invalidates its remaining recovery
  codes and pending challenges, and lets the current sign-in continue.
- With the option cleared and more than one code remaining, M7 asks for
  confirmation, consumes one code on success, and keeps the authenticator and
  settings. The user needs another unused recovery code or the authenticator
  to unlink later.
- Using the final recovery code always performs the full unlink, regardless
  of the checkbox. The server uses the current remaining count, so an older
  screen cannot override this rule.

Invalid or already used recovery codes do not establish proof. After recovery
has unlinked the authenticator, visit settings to enroll the replacement,
choose the desired checks, and generate a fresh recovery set.

## Unlinking when the authenticator is unavailable

The settings page offers unlink proof using a current authenticator code, an
unused recovery code, or an eligible existing linked GitHub or Discord
identity. For linked-account proof, M7 takes the user through the selected
provider and returns to settings for an explicit final confirmation. The
identity must match the already linked account; cancellation does not unlink
the authenticator.

Successful unlink turns all checks off and invalidates the authenticator's
recovery codes and pending challenges. It leaves the identity provider linked
to the M7 account. Provider proof may reuse the provider's session; it does
not guarantee a fresh provider password prompt or provider-side MFA.

For account-management integrations, see the
[API User two-factor contract](https://m7.org/docs/api/api.user.m7.org/two-factor-authentication).
Treat setup keys and recovery codes as secrets. Keep them out of application
logs and URLs, and store recovery codes separately from the authenticator.
