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).

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 — where your login/reset pages live (e.g. http://localhost:3001 in dev).
  • 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.
“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.