Authentication settings
Two separate things control your login experience: the email-link/redirect host (set in the admin Authentication settings) and token verification (set by the platform + your app registration).
API sign-in uses the flow API
Preflight serves only v2. Omit the version header or send Authreads-Login-Flow: 2; other values return unsupported_login_flow. Follow the rotating flow through proof, required setup and final grant exchange.
The legacy email login and old session reverify endpoints were removed on 25 October 2026 in the development release. They return HTTP 410 endpoint_removed. See self-service 2FA for fresh reverify and factor management.
Tenant identifiers and OAuth client authentication
Admin → API and credentials shows both identifiers with copy buttons. Send the publicatr_ identifier in X-Tenant-Id. Signed enrolment receipts and installation revocation payloads contain the UUID in tenant_id and the public ID intenant_public_id. Older already-issued payloads can omit the additive public ID.
For HTTP Basic client authentication, form-encode the client ID and secret separately, join them with a colon, then base64 encode the result. Use either Basic authentication or a form secret. A form client ID supplied alongside Basic must match. Invalid client credentials receive HTTP 401 with a Basic challenge.
An app used only for sign-in should have no role-to-app access binding. Sign-in capability does not grant workspace access; binding it to a role intentionally makes it eligible foraccessible_apps, subject to the remaining access gates.
Set in the admin → Authentication settings
These point your reset/invite/onboarding emails and the redirect-to-login at the right host:
- Auth base URL — the configured origin where your hosted login and reset pages live.
- Password reset / Set password / Accept invite / Onboarding paths — the routes on that host.
400 “set password path is not configured for user pool …” — and every account in the affected user pool is locked out of ever obtaining a first password, silently, until the field is filled in. Configure it during onboarding, before creating your first user — see Onboarding runbook. If your product has no page dedicated to setting a first password, point this at whatever page does handle it — accept-invite and onboarding are common choices, since the same signed token consumes any of them.What makes a token verify (configured elsewhere)
Your product verifies each token against three things — these must line up or every request 401s:
- Issuer (
iss) — the token's issuer must equal your product's expected issuer. - Audience (
aud) — must be one of your registered application keys that your product allows. Register your apps first so tokens carry a matchingaud. - JWKS — your product fetches the signing keys from the platform's
/.well-known/jwks.jsonand matches the token'skid. Keys are Ed25519 (EdDSA); thekidmatch keeps verification working across key rotation.
jwks_uri, and the signing algorithm — is published in the OIDC discovery document at /.well-known/openid-configuration, so a conformant client can configure itself without hard-coding URLs. The token claims themselves are detailed in Sessions & tokens.Verify a token (EdDSA / Ed25519)
Authreads signs tokens with EdDSA (Ed25519) — not the RS256/HS256 that most JWT tutorials assume. Use a library that supports EdDSA and fetches the JWKS for you (jose for JS/TS, PyJWT with cryptography for Python). Both examples cache the key set and refetch automatically when they see a new kid:
import { createRemoteJWKSet, jwtVerify } from "jose";
// Create ONCE and reuse — it caches keys and refetches on an unknown kid.
const JWKS = createRemoteJWKSet(
new URL("https://auth.example.com/.well-known/jwks.json"),
);
const { payload } = await jwtVerify(token, JWKS, {
issuer: "https://auth.example.com", // must equal the token's iss, exactly
audience: "<your-registered-app-key>", // the aud your product allows
algorithms: ["EdDSA"], // pin it — never accept anything else
clockTolerance: "60s", // small leeway for clock skew
});
// payload.sub, payload.app_grants, payload.role, payload.amr, …import jwt
from jwt import PyJWKClient
jwks = PyJWKClient("https://auth.example.com/.well-known/jwks.json") # reuse this client
signing_key = jwks.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=["EdDSA"], # pin the algorithm
issuer="https://auth.example.com",
audience="<your-registered-app-key>",
leeway=60, # clock-skew tolerance, in seconds
)The four mistakes behind almost every verify failure:
- Not pinning the algorithm. Always require
EdDSAand reject everything else. Never allowalg: none, and never let the token's own header choose the algorithm — that's a well-known bypass. - An
iss/audthat only looks right. A trailing slash,httpvshttps, orlocalhostvs127.0.0.1all fail the check even though the strings look identical — the dev base URL is published as127.0.0.1(see Environments & base URLs), and an allowlist carrying the other spelling rejects every token. Copy the exactissuerfrom the discovery document, and setaudto your registered app key. - No clock leeway. Cloud clocks drift; allow 30–60s on
exp/nbf/iat. If you need minutes, fix the clock (sync NTP) instead of widening the window. - Caching JWKS without refetching on a new
kid. On key rotation Authreads publishes the new public key before it signs with it and keeps the previous key in the JWKS during an overlap window — so a verifier that refetches on an unknownkid(the libraries above do) rides through rotation with zero downtime. A verifier that caches on a fixed timer rejects every new token until its cache expires.