Skip to content

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.

POSTLog in/api/v1/auth/login
POSTRefresh the token pair/api/v1/auth/token
POSTLog out/api/v1/auth/logout
POSTChange the password/api/v1/auth/change-password
POSTChange the email address/api/v1/auth/change-email
POSTStart TOTP enrolment/api/v1/auth/2fa/totp/setup
POSTConfirm TOTP enrolment/api/v1/auth/2fa/totp/verify-setup
POSTVerify a TOTP code/api/v1/auth/2fa/totp/verify
POSTRemove the TOTP factor/api/v1/auth/2fa/totp/remove
POSTBegin WebAuthn registration/api/v1/auth/2fa/webauthn/register/begin
POSTFinish WebAuthn registration/api/v1/auth/2fa/webauthn/register/finish
POSTBegin WebAuthn authentication/api/v1/auth/2fa/webauthn/auth/begin
POSTFinish WebAuthn authentication/api/v1/auth/2fa/webauthn/auth/finish
POSTRemove 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 is DPoP, not Bearer.
  • 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_nonce and a DPoP-Nonce response header — retry with that value as the proof's nonce claim, and adopt every rotated nonce a later response carries.
  • Authenticated proofs additionally carry ath (base64url of the access token's SHA-256) and a fresh jti per 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/login

Body

NameDescription
emailstring
passwordstringAccount 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}$
tenantIdentifierstringoptionalPicks 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)

StatusDescriptionBody
409A user with the given email already exists in this tenantcode: TENANT_CONFLICT_ERROR
400The request is malformed or missing required fields
401Invalid credentials or session has expired
429The rate limit for this endpoint was exceeded. The Retry-After header gives the length of the current window in seconds.error: RATE_LIMITED
500The 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/token

Body

NameDescription
refreshTokenstringThe 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)

StatusDescriptionBody
401Invalid credentials or session has expired
400The request did not match the schema for this endpoint. The details field carries the specific failures.error: Validation Error
429The rate limit for this endpoint was exceeded. The Retry-After header gives the length of the current window in seconds.error: RATE_LIMITED
500The 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=false

Query Parameters

NameDescription
alloptionalenumtrue 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: false
Possible values: true, false

Errors

StatusDescriptionBody
401Invalid credentials or session has expired
400The request did not match the schema for this endpoint. The details field carries the specific failures.error: Validation Error
500The 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-password

Body

NameDescription
currentPasswordstringAccount 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}$
newPasswordstringAccount 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

StatusDescriptionBody
401Invalid credentials or session has expired
503Two-factor authentication is temporarily unavailable for this tenant
400The request did not match the schema for this endpoint. The details field carries the specific failures.error: Validation Error
500The 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-email

Body

NameDescription
passwordstringAccount 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

StatusDescriptionBody
401Invalid credentials or session has expired
503Two-factor authentication is temporarily unavailable for this tenant
400The 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
500The 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/setup

Errors

StatusDescriptionBody
401Invalid credentials or session has expired
503Two-factor authentication is temporarily unavailable for this tenant
500The 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-setup

Body

NameDescription
codestringSix-digit TOTP code or 20-character backup code

Errors

StatusDescriptionBody
400The request is malformed or missing required fields
401Invalid credentials or session has expired
503Two-factor authentication is temporarily unavailable for this tenant
500The 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/verify

Body

NameDescription
codestringSix-digit TOTP code or 20-character backup code

Errors

StatusDescriptionBody
401Invalid credentials or session has expired
503Two-factor authentication is temporarily unavailable for this tenant
400The request did not match the schema for this endpoint. The details field carries the specific failures.error: Validation Error
500The 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/remove

Errors

StatusDescriptionBody
401Invalid credentials or session has expired
500The 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/begin

Errors

StatusDescriptionBody
401Invalid credentials or session has expired
500The 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/finish

Body

NameDescription

Errors

StatusDescriptionBody
400The request is malformed or missing required fields
401Invalid credentials or session has expired
500The 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/begin

Errors

StatusDescriptionBody
401Invalid credentials or session has expired
500The 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/finish

Body

NameDescription

Errors

StatusDescriptionBody
400The request is malformed or missing required fields
401Invalid credentials or session has expired
500The 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/remove

Body

NameDescription
credentialIdstringIdentifier of the WebAuthn credential to remove

Errors

StatusDescriptionBody
401Invalid credentials or session has expired
400The request did not match the schema for this endpoint. The details field carries the specific failures.error: Validation Error
500The request failed for a reason that is not the caller's to fix. Safe to retry.error: string, message: string, statusCode: 50