Skip to content

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 is a two-call round-trip so the secret is never stored until you prove you can generate a valid code.

  1. Start setupPOST /api/v1/account/2fa/setup. Pier returns a fresh base32 secret, an otpauth_url, an inline qr_svg, and 10 one-time recovery codes. Nothing is persisted yet.
  2. Scan the QR (or type the secret manually) into your authenticator app. The issuer shown is Pier.
  3. Save your recovery codes. They are shown only once. Each looks like XXXX-XXXX-XXXX and works exactly once.
  4. VerifyPOST /api/v1/account/2fa/verify with the secret, the current 6-digit code, 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.

When 2FA is enabled, login becomes two steps:

StepEndpointYou sendYou get back
1POST /api/v1/auth/loginusername + passwordrequires_2fa: true and a short-lived partial_token
2POST /api/v1/auth/login/2fapartial_token + codethe 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 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-1234 matches ABCDEFGH1234).
  • Each is single-use. When recovery_codes_remaining reaches zero, disable and re-enroll 2FA to get a fresh set.

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.