Skip to content
Authreads Docs

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.
These affect where email links and redirects point — get them wrong and reset/invite emails lead to a dead host. They do not affect whether a token verifies.
If this URL points to a page your product serves, your server owns that page's caching, scripts, framing, and security headers. Apply the self-hosted credential-page baseline before accepting passwords there. Authreads cannot attach headers to a response served by your domain.
“Set password” is not an optional fourth path — it is required for every first-time password, which for a brand-new tenant means every user. A user with no password yet (every user you create without setting one directly, and every fresh tenant's first owner account) has exactly one self-serve route to a password: forgot-password. That flow deliberately treats “no password set” the same as “forgot my password” — same token, same endpoint — and it sends the set-password link, not the reset link, whenever the account has never had one. If the set-password path is blank, that request fails with 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 matching aud.
  • JWKS — your product fetches the signing keys from the platform's /.well-known/jwks.json and matches the token's kid. Keys are Ed25519 (EdDSA); the kid match keeps verification working across key rotation.
Every one of these values — issuer, endpoints, 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.
Minimum for login: register your app key(s), make sure the token issuer matches what your product expects, and that the JWKS endpoint is reachable. The Authentication settings page only sets the email-link/redirect host — not these verification inputs.

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:

Node / TypeScript · jose
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, …
Python · PyJWT
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 EdDSA and reject everything else. Never allow alg: none, and never let the token's own header choose the algorithm — that's a well-known bypass.
  • An iss/aud that only looks right. A trailing slash, http vs https, or localhost vs 127.0.0.1 all fail the check even though the strings look identical — the dev base URL is published as 127.0.0.1 (see Environments & base URLs), and an allowlist carrying the other spelling rejects every token. Copy the exact issuer from the discovery document, and set aud to 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 unknown kid (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.
Verify tokens on your server, never in browser JavaScript — a browser can't keep anything secret and shouldn't be the thing deciding whether a token is valid. See what's safe in the browser.