Skip to content

Enable MFA (TOTP) and the admin MFA policy

Repod supports TOTP-based multi-factor authentication (Google Authenticator, Authy, 1Password, any RFC 6238-compatible app) for any user account, plus an opt-in policy that requires admin accounts to have MFA enabled, with a grace period before enforcement kicks in.

This page covers two independent scenarios:

  1. Any user enabling MFA for their own account (self-service, no special permission needed).
  2. An administrator turning on mandatory MFA for admin accounts, and what happens to an admin who hasn't set it up yet.

Part A — Enable MFA for your own account

1. Prerequisites

  • A logged-in Repod session (any role).
  • A TOTP authenticator app on your phone or password manager.

2. Step 1 — Request a TOTP secret

curl -X POST https://repod.example.com/api/v1/auth/mfa/setup \
  -H "Authorization: Bearer $JWT"
{
  "secret": "JBSWY3DPEHPK3PXP",
  "uri": "otpauth://totp/repod:alice?secret=JBSWY3DPEHPK3PXP&issuer=repod",
  "qr_code_base64": "iVBORw0KGgoAAAANSUhEUgA..."
}

The secret is stored pending — it is not active yet. In the UI, this step renders the qr_code_base64 PNG as a scannable QR code. Scan it with your authenticator app, or enter the secret manually if scanning isn't possible.

3. Step 2 — Confirm with a TOTP code

Enter the 6-digit code your authenticator app now shows:

curl -X POST https://repod.example.com/api/v1/auth/mfa/confirm \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{"totp_code": "482913"}'
{
  "message": "MFA activé avec succès. Conservez vos codes de récupération en lieu sûr.",
  "recovery_codes": ["3F7K-9QRT", "8P2M-XW4L", "…"]
}

MFA is now active. Save the 8 recovery codes somewhere safe — each is single-use and lets you log in if you lose access to your authenticator app. They are shown exactly once, at confirmation time; they are never retrievable again except by regenerating a fresh set (which invalidates the old ones).

Recovery codes are shown only once

If you lose them, the only way to get a new set is POST /api/v1/auth/mfa/recovery-codes (requires your password) — which invalidates every previously issued code.

4. Logging in with MFA enabled

Once MFA is active, POST /api/v1/auth/token no longer returns an access_token directly — it returns a short-lived MFA challenge instead:

{
  "mfa_required": true,
  "mfa_token": "eyJhbGciOi...",
  "token_type": "bearer"
}

Submit the TOTP code (or a recovery code) against that token to complete login:

curl -X POST https://repod.example.com/api/v1/auth/mfa/authenticate \
  -H "Content-Type: application/json" \
  -d '{"mfa_token": "eyJhbGciOi...", "totp_code": "482913"}'
{"access_token": "eyJhbGciOi...", "token_type": "bearer"}

To use a recovery code instead of a TOTP code, send "recovery_code": "3F7K-9QRT" instead of totp_code. The mfa_token itself expires after 5 minutes.

5. Disabling MFA

curl -X POST https://repod.example.com/api/v1/auth/mfa/disable \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{"password": "your-current-password"}'

Requires your current password as confirmation.

Verify it worked (Part A)

  1. GET /api/v1/auth/me should show "mfa_enabled": true.
  2. GET /api/v1/auth/mfa/recovery-codes/count should show {"remaining": 8} right after setup, decreasing by one each time a recovery code is used.
  3. Log out and log back in — you should be prompted for a TOTP code before receiving an access token.

Part B — Mandatory admin MFA with grace period

This policy forces every admin account to enable MFA, without ever locking anyone out completely. It is opt-in and disabled by default — turning it on never breaks an existing deployment where admins haven't set up MFA yet, because of the grace period below.

1. Prerequisites

  • admin access to Settings.

2. Step 1 — Enable the policy

curl -X PATCH https://repod.example.com/api/v1/settings/ \
  -H "Authorization: Bearer $ADMIN_JWT" \
  -H "Content-Type: application/json" \
  -d '{"mfa_policy": {"enforce_admin_mfa": true, "grace_period_days": 7}}'
Setting Default Meaning
enforce_admin_mfa false Master switch. false = policy has no effect at all, regardless of grace_period_days.
grace_period_days 7 Days an admin without MFA can keep logging in normally after the policy is turned on, before being blocked from normal login.

3. What happens to an admin account without MFA

The grace period clock starts at the admin's first login after the policy is turned on — never at account creation. An admin created two years ago isn't retroactively penalized by their account age the day the policy is activated; a brand-new admin gets the exact same window starting from their own first login. One mechanism covers both cases.

Timeline for an admin account with no MFA, once enforce_admin_mfa: true:

  1. First login after activation — login succeeds normally, and the grace clock starts (users.mfa_grace_started_at is set once, server-side).
  2. During the grace window — login continues to succeed normally. GET /api/v1/auth/me includes an mfa_grace field so the UI can show a warning banner:
    {"username": "bob", "role": "admin", "mfa_grace": {"days_remaining": 4}}
    
  3. After the grace window expiresPOST /api/v1/auth/token no longer issues a normal access token. It instead returns:
    {"mfa_setup_required": true, "mfa_setup_token": "eyJhbGciOi...", "token_type": "bearer"}
    

Never a full lockout

The mfa_setup_token (scope mfa_setup_required, 30-minute expiry) is rejected by every normal endpoint — it authorizes only POST /api/v1/auth/mfa/setup and POST /api/v1/auth/mfa/confirm. A blocked admin can always finish activating MFA themselves; there is no state where an admin is locked out with no path forward.

4. Completing setup with a restricted token

An admin who received mfa_setup_required follows the exact same two calls as Part A, just using mfa_setup_token as the bearer token:

curl -X POST https://repod.example.com/api/v1/auth/mfa/setup \
  -H "Authorization: Bearer $MFA_SETUP_TOKEN"
curl -X POST https://repod.example.com/api/v1/auth/mfa/confirm \
  -H "Authorization: Bearer $MFA_SETUP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"totp_code": "482913"}'

Because the caller had no other valid session (the setup token grants nothing else), mfa_confirm detects this case and returns a real access_token directly in the same response — no second login required:

{
  "message": "MFA activé avec succès. Conservez vos codes de récupération en lieu sûr.",
  "recovery_codes": ["…", "…"],
  "access_token": "eyJhbGciOi...",
  "token_type": "bearer"
}

Verify it worked (Part B)

  1. Confirm the policy is saved: GET /api/v1/settings/mfa_policy.enforce_admin_mfa is true.
  2. Log in as an admin without MFA — the response should be a normal access_token (grace period active), and GET /api/v1/auth/me should show mfa_grace.days_remaining counting down.
  3. To test the expired-grace path without waiting a week, temporarily set grace_period_days: 0 and log in again as that same admin — you should get mfa_setup_required instead of access_token.
  4. Complete setup using the mfa_setup_token as shown above, and confirm the response includes a working access_token.
  5. Restore grace_period_days to your intended value afterward.

Admins who already have MFA are unaffected

The pre-existing MFA login flow (mfa_requiredPOST /api/v1/auth/mfa/authenticate) always runs first and returns before this policy is ever evaluated — an admin with MFA already enabled never sees mfa_setup_required.