Skip to content
Slicekit

Backend guides

Two-factor authentication

How authenticator enrollment, sign-in challenges and recovery codes work alongside passkeys.

View .md
On this page

Slicekit supports authenticator apps using TOTP (Time-based One-Time Password, RFC 6238) and passkeys. An account has a second factor when its authenticator is confirmed or it has a passkey that counts as a factor. Each method counts only while its feature is enabled. Recovery codes belong to the authenticator; a passkey-only account has none. See Authentication for the password and session flow.

Feature settings

Auth:TotpEnabled controls authenticator endpoints and UI. Disabling it leaves passkeys available when Auth:PasskeysEnabled is on. Auth:TotpRequired requires enrollment in at least one enabled second-factor method before ordinary account operations can continue.

"Auth": {
  "TotpEnabled": true,
  "TotpRequired": false
}

Override settings through environment variables such as Auth__TotpEnabled=false. GET /api/v1/auth/features reports which methods the deployment offers. The endpoint registration applies the same gates. Setup routes opt out of the required-enrollment filter so users can add a factor.

Endpoint contracts

Each operation is a vertical slice in Slicekit.Core/Features/Auth/ with a thin endpoint in Slicekit.Api/Endpoints/v1/. See Adding a vertical slice for that pattern. The endpoint files and the API’s OpenAPI reference define the current request and response contracts.

Authenticator setup, confirmation, disabling and recovery-code regeneration use the /api/v1/users/me/totp route family. Sign-in verification and recovery use /api/v1/auth/totp.

Enrollment

The user must be signed in, have a confirmed email, and hold the matching security permissions. Cookie requests also require CSRF protection.

  1. Begin with POST /api/v1/users/me/totp/setup, supplying a password or step-up proof. Setup refuses when the authenticator is already enabled, so a session cannot replace a confirmed factor. It returns secretBase32 and qrCodeUri for the authenticator app; provisioning alone does not enable the factor.
  2. Scan the QR code, then send the first code to POST /api/v1/users/me/totp/confirm. Confirmation enables the authenticator and returns recovery codes.
  3. Save those recovery codes. They are displayed once, and their plaintext cannot be retrieved later.

The recovery-code dialog must outlive the setup card. Confirmation refreshes the current user and replaces that card, so a dialog inside it would close before the user could save the codes.

Sign-in challenge

When a second factor is required at sign-in, the API returns TotpRequired with a short-lived pendingToken, totpAvailable and passkeyAvailable. Offer only the methods available to that account. The pending token cannot authenticate normal API requests. It binds the account’s credential generation. Password changes, recovery, confirmed email changes and admin second-factor resets invalidate earlier challenges; the user must start sign-in again.

For an authenticator, send pendingToken and code to POST /api/v1/auth/totp/verify. A successful verification returns 204 No Content with the session cookies set. Passkey challenges use the passkey verification flow instead.

Passkey assertions finalize against the latest stored signature counter under a credential row lock across sign-in, second-factor verification and reauthentication. Positive counters must increase, including across concurrent ceremonies. Authenticators that always report zero remain supported.

Recovery and regeneration

If the authenticator is lost, send pendingToken and recoveryCode to POST /api/v1/auth/totp/recover. Each recovery code works once and is consumed on successful sign-in. The API stores hashes, with the Identity token column also encrypted at rest.

Signed-in users can replace their recovery-code set through POST /api/v1/users/me/totp/recovery-codes after step-up verification. Save the new set; regeneration invalidates the previous codes.

Disabling

DELETE /api/v1/users/me/totp requires a password or single-use step-up proof. It clears the authenticator key and recovery codes. When second-factor enrollment is mandatory, disabling is refused if it would remove the account’s last enabled factor. Re-enrollment starts with a new secret.

Throttling

Verification and recovery use the shared auth rate-limit policy and per-account attempt limits: Auth:MaxTotpAttemptsPerAccountWindow and Auth:MaxRecoveryCodeAttemptsPerAccountWindow. Exhausting either budget returns a lockout response; successful verification clears its attempt counter. See Rate limiting for deployment settings.

Checklist

  • Set the feature flags and decide whether enrollment is mandatory.
  • Complete setup and confirmation, then save the recovery codes before leaving the screen.
  • Offer only the challenge methods returned by the sign-in response.
  • Verify recovery codes are consumed and regeneration invalidates the old set.
  • Require step-up before disabling and preserve the last-factor rule.

Live demo

Explore the running app

No sign-up. Pick an account to open the demo with its credentials prefilled, or open it and sign in yourself.

Open without signing in

Credentials are prefilled. Shared password Slicekit-Demo-2026!.