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:
- Any user enabling MFA for their own account (self-service, no special permission needed).
- 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¶
{
"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:
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"}'
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)¶
GET /api/v1/auth/meshould show"mfa_enabled": true.GET /api/v1/auth/mfa/recovery-codes/countshould show{"remaining": 8}right after setup, decreasing by one each time a recovery code is used.- 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¶
adminaccess 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:
- First login after activation — login succeeds normally, and the grace clock starts (
users.mfa_grace_started_atis set once, server-side). - During the grace window — login continues to succeed normally.
GET /api/v1/auth/meincludes anmfa_gracefield so the UI can show a warning banner: - After the grace window expires —
POST /api/v1/auth/tokenno longer issues a normal access token. It instead returns:
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)¶
- Confirm the policy is saved:
GET /api/v1/settings/→mfa_policy.enforce_admin_mfaistrue. - Log in as an admin without MFA — the response should be a normal
access_token(grace period active), andGET /api/v1/auth/meshould showmfa_grace.days_remainingcounting down. - To test the expired-grace path without waiting a week, temporarily set
grace_period_days: 0and log in again as that same admin — you should getmfa_setup_requiredinstead ofaccess_token. - Complete setup using the
mfa_setup_tokenas shown above, and confirm the response includes a workingaccess_token. - Restore
grace_period_daysto your intended value afterward.
Admins who already have MFA are unaffected
The pre-existing MFA login flow (mfa_required → POST /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.