:::endpoint POST /v1/auth/token/two-factor

The second half of a sign-in. Send the `challenge` from
[the first step](/docs/api/apps/sign-in) with one of three answers: a code from
an authenticator app, one of the account's recovery codes, or a passkey
assertion. Which of them the account can use is in `methods` on the challenge.

Everything else about the sign-in, including who is signing in, which app and
which scopes, comes from the challenge. This request cannot change any of it.

## Request

```bash
curl https://api.proppertrading.com/v1/auth/token/two-factor \
  -H "Content-Type: application/json" \
  -d '{
    "challenge": "a14ea608e89261062f4e90792701ef2a7c5ad1188c8537108efd788d1390ce03",
    "code": "123456"
  }'
```

| Field           | Type   | Description                                                                                    |
| --------------- | ------ | ---------------------------------------------------------------------------------------------- |
| `challenge`     | string | From the sign-in response. Required                                                            |
| `code`          | string | Six digits from the authenticator. Required without `recovery_code`                            |
| `recovery_code` | string | One of the codes handed out when two-factor was set up. Required without `code`                |
| `passkey`       | object | A WebAuthn assertion, exactly as the authenticator returned it. Required without the other two |

A recovery code is spent when it is used, exactly as on the web. Nine remain
after the first one.

Send one of the three, not several. The `passkey` object is the assertion with
`id`, `rawId`, `type` and a `response` holding `clientDataJSON`,
`authenticatorData`, `signature` and optionally `userHandle`. Ask
[for its options](/docs/api/apps/two-factor-passkey-options) first: the
assertion has to answer a challenge this API issued.

## Response

Identical to a successful [sign-in](/docs/api/apps/sign-in), with
`session.two_factor` set to `true`.

```json
{
    "data": {
        "object": "access_token",
        "status": "signed_in",
        "token": "ptat_9f2c1a44_3b8e7d2f5c9a1b4e6d8f0a2c4e6b8d1f",
        "session": { "id": "9f2c1a44", "object": "session", "two_factor": true }
    },
    "meta": { "request_id": "req_01kz4bkr1aj1gv63d8fv3133ey" }
}
```

## The challenge is used up either way

It survives exactly one correct code, and at most five wrong ones. After that it
is gone and the sign-in starts again from the password. A client that hits
`expired_two_factor_challenge` should return to its email and password screen
rather than asking for another code.

Challenges also expire on their own after five minutes.

## Errors

| Status | Code                           | Meaning                                                                   |
| ------ | ------------------------------ | ------------------------------------------------------------------------- |
| 401    | `invalid_two_factor_code`      | Wrong code, or a passkey that did not verify. The challenge is still good |
| 401    | `expired_two_factor_challenge` | Expired, already used, or burned by five wrong codes                      |
| 401    | `account_deactivated`          | The account was switched off between the two steps                        |
| 403    | `block_anonymizer`             | The firm blocks VPN, proxy and Tor connections                            |
| 403    | `block_country`                | The firm blocks connections from this country                             |
| 422    | `validation_failed`            | None of a code, a recovery code or a passkey was sent                     |
| 429    | `rate_limit_exceeded`          | Twenty attempts per ten minutes per address                               |
| 503    | `tenant_in_maintenance`        | The firm closed between the two steps                                     |

:::note Every check runs again
Minutes can pass between the password and the code, and an account can be
deactivated or a firm closed in between. The second step is not a formality.
:::