Appearance
IAM — Authentication
Everything here acts on an account that already exists — logging in, keeping the session alive, changing the credentials on it. A session is identified by the client's DPoP key pair, not by an id of its own. An administrator can demand a second factor, but only the account holder can enrol or remove one.
Accounts, invitations and password resets belong to users; what a session may do is assembled at each request from the account's own rules plus every role it holds, not frozen into the token. Enrolment and password writes raise IAM_USER_ENROLL_TOTP, IAM_USER_ENROLL_WEBAUTHN_CREDENTIAL and IAM_USER_SET_PASSWORD. The websocket takes a ticket minted from a live session.
| POST | Log in/api/v1/auth/login |
| POST | Refresh the token pair/api/v1/auth/token |
| POST | Log out/api/v1/auth/logout |
| POST | Change the password/api/v1/auth/change-password |
| POST | Change the email address/api/v1/auth/change-email |
| POST | Start TOTP enrolment/api/v1/auth/2fa/totp/setup |
| POST | Confirm TOTP enrolment/api/v1/auth/2fa/totp/verify-setup |
| POST | Verify a TOTP code/api/v1/auth/2fa/totp/verify |
| POST | Remove the TOTP factor/api/v1/auth/2fa/totp/remove |
| POST | Begin WebAuthn registration/api/v1/auth/2fa/webauthn/register/begin |
| POST | Finish WebAuthn registration/api/v1/auth/2fa/webauthn/register/finish |
| POST | Begin WebAuthn authentication/api/v1/auth/2fa/webauthn/auth/begin |
| POST | Finish WebAuthn authentication/api/v1/auth/2fa/webauthn/auth/finish |
| POST | Remove a WebAuthn credential/api/v1/auth/2fa/webauthn/remove |
Log in
Login issues DPoP-bound tokens (RFC 9449): the client holds an EC key pair, every request carries a short-lived signed proof in the DPoP header, and issued tokens are bound to that key's thumbprint — a stolen token is useless without the private key. The essentials:
Authorization: DPoP <accessToken>— the scheme isDPoP, notBearer.- One key pair per session: login, every request and every refresh must use the same key.
- The server requires a nonce in every proof. The first call is answered with
400 use_dpop_nonceand aDPoP-Nonceresponse header — retry with that value as the proof'snonceclaim, and adopt every rotated nonce a later response carries. - Authenticated proofs additionally carry
ath(base64url of the access token's SHA-256) and a freshjtiper request. - Access tokens live 10 minutes; refresh tokens live 8 hours and rotate on every use.
If the email exists in more than one tenant reachable under the request host, the password picks the account; only when it matches several does the route answer 409 with the candidate tenants — retry with tenantIdentifier.
A complete client — key handling, nonce handshake, refresh on 401, downloadable example — is in the guide chapter Authentication & Sessions.
Exchanges credentials for a DPoP-bound token pair; the request needs a valid DPoP proof. The answer may instead be 2fa_required, carrying a short-lived accessToken for the /auth/2fa routes, or email_verification_required; tenantIdentifier picks the tenant when the address holds an account in several on this host.
POST
/api/v1/auth/loginBody
| Name | Description |
|---|---|
emailstring | |
passwordstring | Account password: 16 to 64 characters, with no character-class rules on top. The same bound holds at registration, reset and change, so a shorter one is refused before it can ever be set. min length 16 · max length 64 Pattern: ^[\s\S]{16,64}$ |
tenantIdentifierstringoptional | Picks the tenant when the address holds an account in more than one reachable on this host; accepts the bare tenant id or the crn#tenant:<id> form the conflict response returns. |
Errors
Rate limited.
50 requests / 900 s per ip (login-per-ip)
10 requests / 900 s per ip (login-per-ip-per-email)
| Status | Description | Body |
|---|---|---|
409 | A user with the given email already exists in this tenant | code: TENANT_CONFLICT_ERROR |
400 | The request is malformed or missing required fields | |
401 | Invalid credentials or session has expired | |
429 | The rate limit for this endpoint was exceeded. The Retry-After header gives the length of the current window in seconds. | error: RATE_LIMITED |
500 | The request failed for a reason that is not the caller's to fix. Safe to retry. | error: string, message: string, statusCode: 50 |
Refresh the token pair
Consumes the presented refresh token and issues a new pair bound to the same DPoP key — a token is never usable twice. A session that never presented a second factor is refused once 2FA has become required for the user, and has to log in again.
POST
/api/v1/auth/tokenBody
| Name | Description |
|---|---|
refreshTokenstring | The refresh token of the session to continue; single-use, the call consumes it and returns a replacement. |
Errors
Rate limited.
60 requests / 900 s per ip (token-per-ip)
| Status | Description | Body |
|---|---|---|
401 | Invalid credentials or session has expired | |
400 | The request did not match the schema for this endpoint. The details field carries the specific failures. | error: Validation Error |
429 | The rate limit for this endpoint was exceeded. The Retry-After header gives the length of the current window in seconds. | error: RATE_LIMITED |
500 | The request failed for a reason that is not the caller's to fix. Safe to retry. | error: string, message: string, statusCode: 50 |
Log out
Ends the session the presented token belongs to; all=true ends every session the user has. A half-finished login is accepted too — abandoning a pending_2fa token is exactly when a client needs this, and it is still a live credential.
POST
/api/v1/auth/logout?all=falseQuery Parameters
| Name | Description |
|---|---|
alloptionalenum | true ends every session of the user instead of only the one the request authenticates with; only the literal strings true and false are accepted, and the default is false.Default: falsePossible values: true, false |
Errors
| Status | Description | Body |
|---|---|---|
401 | Invalid credentials or session has expired | |
400 | The request did not match the schema for this endpoint. The details field carries the specific failures. | error: Validation Error |
500 | The request failed for a reason that is not the caller's to fix. Safe to retry. | error: string, message: string, statusCode: 50 |
Change the password
Takes the current password alongside the new one and then ends every session, this one included — no fresh token pair comes back, so the client has to log in again. A pending_2fa access token is rejected: the session must be fully authenticated.
POST
/api/v1/auth/change-passwordBody
| Name | Description |
|---|---|
currentPasswordstring | Account password: 16 to 64 characters, with no character-class rules on top. The same bound holds at registration, reset and change, so a shorter one is refused before it can ever be set. min length 16 · max length 64 Pattern: ^[\s\S]{16,64}$ |
newPasswordstring | Account password: 16 to 64 characters, with no character-class rules on top. The same bound holds at registration, reset and change, so a shorter one is refused before it can ever be set. min length 16 · max length 64 Pattern: ^[\s\S]{16,64}$ |
Errors
| Status | Description | Body |
|---|---|---|
401 | Invalid credentials or session has expired | |
503 | Two-factor authentication is temporarily unavailable for this tenant | |
400 | The request did not match the schema for this endpoint. The details field carries the specific failures. | error: Validation Error |
500 | The request failed for a reason that is not the caller's to fix. Safe to retry. | error: string, message: string, statusCode: 50 |
Change the email address
Asks for the account password again, then points the account at newEmail. The address starts unverified and a verification code goes out at once, so the answer is email_verification_required; an address already holding an account in the tenant is refused.
POST
/api/v1/auth/change-emailBody
| Name | Description |
|---|---|
passwordstring | Account password: 16 to 64 characters, with no character-class rules on top. The same bound holds at registration, reset and change, so a shorter one is refused before it can ever be set. min length 16 · max length 64 Pattern: ^[\s\S]{16,64}$ |
newEmailstring |
Errors
| Status | Description | Body |
|---|---|---|
401 | Invalid credentials or session has expired | |
503 | Two-factor authentication is temporarily unavailable for this tenant | |
400 | The email address already has an account in this tenant — invite a different address, or reset the password of the existing account. | code: USER_ALREADY_EXISTS |
500 | The request failed for a reason that is not the caller's to fix. Safe to retry. | error: string, message: string, statusCode: 50 |
Start TOTP enrolment
Mints a pending TOTP secret and returns it as the otpauth:// URI an authenticator app scans. Nothing is enrolled yet — the secret is held for five minutes and only /2fa/totp/verify-setup makes it the account's factor.
POST
/api/v1/auth/2fa/totp/setupErrors
| Status | Description | Body |
|---|---|---|
401 | Invalid credentials or session has expired | |
503 | Two-factor authentication is temporarily unavailable for this tenant | |
500 | The request failed for a reason that is not the caller's to fix. Safe to retry. | error: string, message: string, statusCode: 50 |
Confirm TOTP enrolment
Confirms the pending secret with a code from the app and enrols the factor. The backup codes come back once and are stored hashed — there is no second chance to read them; a login still in pending_2fa_setup also gets a fresh accessToken and finishes at /2fa/totp/verify.
POST
/api/v1/auth/2fa/totp/verify-setupBody
| Name | Description |
|---|---|
codestring | Six-digit TOTP code or 20-character backup code |
Errors
| Status | Description | Body |
|---|---|---|
400 | The request is malformed or missing required fields | |
401 | Invalid credentials or session has expired | |
503 | Two-factor authentication is temporarily unavailable for this tenant | |
500 | The request failed for a reason that is not the caller's to fix. Safe to retry. | error: string, message: string, statusCode: 50 |
Verify a TOTP code
Finishes a pending_2fa login and returns the full token pair. Takes a six-digit code — replayed inside its window it is refused — or one of the 20-character backup codes, which is then spent.
POST
/api/v1/auth/2fa/totp/verifyBody
| Name | Description |
|---|---|
codestring | Six-digit TOTP code or 20-character backup code |
Errors
| Status | Description | Body |
|---|---|---|
401 | Invalid credentials or session has expired | |
503 | Two-factor authentication is temporarily unavailable for this tenant | |
400 | The request did not match the schema for this endpoint. The details field carries the specific failures. | error: Validation Error |
500 | The request failed for a reason that is not the caller's to fix. Safe to retry. | error: string, message: string, statusCode: 50 |
Remove the TOTP factor
Drops the enrolled secret together with its backup codes. A WebAuthn credential left on the account still answers the next login with 2fa_required and setupRequired: false; only where no factor at all remains and the tenant, a role or the account still enforces 2FA does the answer carry setupRequired: true.
POST
/api/v1/auth/2fa/totp/removeErrors
| Status | Description | Body |
|---|---|---|
401 | Invalid credentials or session has expired | |
500 | The request failed for a reason that is not the caller's to fix. Safe to retry. | error: string, message: string, statusCode: 50 |
Begin WebAuthn registration
Returns the creation options for navigator.credentials.create(), with the credentials already enrolled on the account excluded. The challenge lives two minutes and nothing is stored until the finish call.
POST
/api/v1/auth/2fa/webauthn/register/beginErrors
| Status | Description | Body |
|---|---|---|
401 | Invalid credentials or session has expired | |
500 | The request failed for a reason that is not the caller's to fix. Safe to retry. | error: string, message: string, statusCode: 50 |
Finish WebAuthn registration
Takes the authenticator's response verbatim and stores the credential — an account may hold several. From pending_2fa_setup a fresh accessToken comes back and the login is still finished over /2fa/webauthn/auth/*.
POST
/api/v1/auth/2fa/webauthn/register/finishBody
| Name | Description |
|---|
Errors
| Status | Description | Body |
|---|---|---|
400 | The request is malformed or missing required fields | |
401 | Invalid credentials or session has expired | |
500 | The request failed for a reason that is not the caller's to fix. Safe to retry. | error: string, message: string, statusCode: 50 |
Begin WebAuthn authentication
Returns the request options for navigator.credentials.get(), listing the credentials the account has enrolled. Only a pending_2fa access token is accepted, and an account with no enrolled credential is answered with 401.
POST
/api/v1/auth/2fa/webauthn/auth/beginErrors
| Status | Description | Body |
|---|---|---|
401 | Invalid credentials or session has expired | |
500 | The request failed for a reason that is not the caller's to fix. Safe to retry. | error: string, message: string, statusCode: 50 |
Finish WebAuthn authentication
Verifies the assertion, records the credential's new signature counter and returns the full token pair. The id in the body has to name a credential enrolled on this account.
POST
/api/v1/auth/2fa/webauthn/auth/finishBody
| Name | Description |
|---|
Errors
| Status | Description | Body |
|---|---|---|
400 | The request is malformed or missing required fields | |
401 | Invalid credentials or session has expired | |
500 | The request failed for a reason that is not the caller's to fix. Safe to retry. | error: string, message: string, statusCode: 50 |
Remove a WebAuthn credential
Removes one credential by id; the WebAuthn factor itself disappears with its last one. It needs a fully authenticated session, not a pending_2fa token.
POST
/api/v1/auth/2fa/webauthn/removeBody
| Name | Description |
|---|---|
credentialIdstring | Identifier of the WebAuthn credential to remove |
Errors
| Status | Description | Body |
|---|---|---|
401 | Invalid credentials or session has expired | |
400 | The request did not match the schema for this endpoint. The details field carries the specific failures. | error: Validation Error |
500 | The request failed for a reason that is not the caller's to fix. Safe to retry. | error: string, message: string, statusCode: 50 |