Two-factor auth
Pier supports optional TOTP (time-based one-time password) two-factor auth, compatible with any authenticator app — Google Authenticator, Aegis, 1Password, and others. Enabling it adds a second step to your login: after your password, you present a 6-digit code or a one-time recovery code.
2FA is enrolled per user from the Account area. It does not change how sessions work — once you pass the second factor, you get the same opaque cookie session as a password-only login.
Enrollment
Section titled “Enrollment”Enrollment is a two-call round-trip so the secret is never stored until you prove you can generate a valid code.
- Start setup —
POST /api/v1/account/2fa/setup. Pier returns a fresh base32secret, anotpauth_url, an inlineqr_svg, and 10 one-time recovery codes. Nothing is persisted yet. - Scan the QR (or type the secret manually) into your authenticator app. The issuer shown is
Pier. - Save your recovery codes. They are shown only once. Each looks like
XXXX-XXXX-XXXXand works exactly once. - Verify —
POST /api/v1/account/2fa/verifywith thesecret, the current 6-digitcode, and the recovery hashes. On a correct code, Pier encrypts the secret (AES-256-GCM, same key as your env vars) and commits it. A wrong code leaves 2FA off.
The TOTP code is verified with a ±1 step (±30 s) window, so a small clock drift on your phone is tolerated.
You can re-check enrollment state any time with GET /api/v1/account/2fa, which reports enabled, enabled_at, and recovery_codes_remaining.
The login flow with 2FA
Section titled “The login flow with 2FA”When 2FA is enabled, login becomes two steps:
| Step | Endpoint | You send | You get back |
|---|---|---|---|
| 1 | POST /api/v1/auth/login | username + password | requires_2fa: true and a short-lived partial_token |
| 2 | POST /api/v1/auth/login/2fa | partial_token + code | the session cookie |
The partial_token is not a session — it only proves you cleared the password step. It is a one-shot token held in memory, expires after 5 minutes, and is bound to your IP. If pier-core restarts in between, retype your password.
In step 2, the code field accepts either your current TOTP code or one of your recovery codes. A recovery code is consumed (removed) on use, so it never works twice.
Both login endpoints are rate-limited to slow brute-force attempts against the 6-digit code.
Recovery codes
Section titled “Recovery codes”Recovery codes are your fallback when you lose your authenticator device.
- 10 are generated at enrollment; only their hashes are stored.
- They are case- and dash-insensitive on entry (
abcd-EFGH-1234matchesABCDEFGH1234). - Each is single-use. When
recovery_codes_remainingreaches zero, disable and re-enroll 2FA to get a fresh set.
Disabling 2FA
Section titled “Disabling 2FA”POST /api/v1/account/2fa/disable turns 2FA off, but it requires a valid current TOTP code or a recovery code in the request body. This prevents someone who has hijacked a live session from silently weakening your account. On success, the secret and recovery codes are cleared.
To rotate your secret (for example, after moving to a new phone), disable 2FA and enroll again — Pier refuses to re-issue a secret while 2FA is still active.