Answer a two-factor challenge

.md

Turn a challenge and a code into a token.

POST/v1/auth/token/two-factor

The second half of a sign-in. Send the challenge from the first step 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

curl https://api.proppertrading.com/v1/auth/token/two-factor \
  -H "Content-Type: application/json" \
  -d '{
    "challenge": "a14ea608e89261062f4e90792701ef2a7c5ad1188c8537108efd788d1390ce03",
    "code": "123456"
  }'
FieldTypeDescription
challengestringFrom the sign-in response. Required
codestringSix digits from the authenticator. Required without recovery_code
recovery_codestringOne of the codes handed out when two-factor was set up. Required without code
passkeyobjectA 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 first: the assertion has to answer a challenge this API issued.

Response

Identical to a successful sign-in, with session.two_factor set to true.

{
    "data": {
        "object": "access_token",
        "status": "signed_in",
        "token": "ptat_9f2c1a44_3b8e7d2f5c9a1b4e6d8f0a2c4e6b8d1f",
        "session": { "id": "9f2c1a44", "object": "session", "two_factor": true },
        "workspace": { "object": "workspace", "id": "acmefunded", "name": "Acme Funded", "platform": false, "url": "https://acmefunded.com", "branding": null, "maintenance": false }
    },
    "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

StatusCodeMeaning
401invalid_two_factor_codeWrong code, or a passkey that did not verify. The challenge is still good
401expired_two_factor_challengeExpired, already used, or burned by five wrong codes
401account_deactivatedThe account was switched off between the two steps
403admin_access_requiredSigning in to the Propper app with an account that does not help run the environment
403block_anonymizerThe firm blocks VPN, proxy and Tor connections
403block_countryThe firm blocks connections from this country
422validation_failedNone of a code, a recovery code or a passkey was sent
429rate_limit_exceededTwenty attempts per ten minutes per address
503tenant_in_maintenanceThe firm closed between the two steps

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.