# Two-factor authentication (2FA)

Source: https://developers.leonai.io/docs/guides/two-factor/

LeonAI supports **TOTP** (RFC 6238) two-factor authentication for account logins. The
TOTP secret is stored AES-256-GCM encrypted server-side, and enrollment yields ten
single-use recovery codes. All 2FA management endpoints require a **JWT**; the final
`verify` step during login is public (it is the second half of the login handshake).

:::note[Server requirement]
2FA enrollment needs a `TOTP_ENCRYPTION_KEY` configured on the server. Without it,
`POST /auth/2fa/setup` returns `503`.
:::

## Enroll

### 1. Setup — get a secret

```bash
curl -X POST https://api.short.leonai.io/api/v1/auth/2fa/setup \
  -H "Authorization: Bearer <jwt>"
```

`REST: POST /api/v1/auth/2fa/setup` → `{ success, data: { secret, otpauthUrl } }`. The
plaintext `secret` (and the `otpauthUrl` for a QR code) is revealed **only here**. Load
it into an authenticator app (Google Authenticator, 1Password, Authy, …).

### 2. Enable — confirm a code

```bash
curl -X POST https://api.short.leonai.io/api/v1/auth/2fa/enable \
  -H "Authorization: Bearer <jwt>" \
  -H "Content-Type: application/json" \
  -d '{ "token": "123456" }'
```

`REST: POST /api/v1/auth/2fa/enable`. `token` is the current 6-digit code
(`^\d{6}$`). On success:

```json
{ "success": true, "data": { "enabled": true, "recoveryCodes": ["…", "…"] } }
```

The **ten recovery codes are shown only once** — store them somewhere safe. Each is
single-use.

## Login with 2FA

When 2FA is enabled, `POST /api/v1/auth/login` no longer returns tokens. Instead it
returns a short-lived (5-minute) pending token:

```json
{ "success": true, "data": { "requiresTwoFactor": true, "pendingToken": "…" } }
```

Complete the login by verifying a factor:

```bash
curl -X POST https://api.short.leonai.io/api/v1/auth/2fa/verify \
  -H "Content-Type: application/json" \
  -d '{ "pendingToken": "…", "token": "123456" }'
```

`REST: POST /api/v1/auth/2fa/verify` (no auth header). Send **either** `token` (6-digit
TOTP) **or** `recoveryCode` (a stored recovery code, single-use). On success it returns
the normal login payload — `{ user, tokens: { accessToken, refreshToken, expiresIn } }`.
The pending token is single-use. This endpoint is rate limited (20/5 min per IP, plus a
per-user failure counter).

## Status & disable

```bash
# Is 2FA on?
curl https://api.short.leonai.io/api/v1/auth/2fa/status -H "Authorization: Bearer <jwt>"

# Turn it off (provide a current TOTP code OR a recovery code)
curl -X POST https://api.short.leonai.io/api/v1/auth/2fa/disable \
  -H "Authorization: Bearer <jwt>" \
  -H "Content-Type: application/json" \
  -d '{ "token": "123456" }'
```

- `GET /api/v1/auth/2fa/status` → `{ success, data: { enabled: boolean } }`.
- `POST /api/v1/auth/2fa/disable` requires **at least one** of `token` or
  `recoveryCode`.

## Related

- [Authentication](https://developers.leonai.io/docs/authentication/) — the JWT login/refresh flow this builds on
