Sign in with authreads
Standard OpenID Connect against the authreads-hosted login page. Use this when you want a drop-in “Sign in with authreads” button instead of building your own login UI — any conformant OIDC client library works, with the caveats below.
dev-email feature and configured with AUTH_ENV=dev plus AUTH_EMAIL_TRANSPORT=console. OTPs and password links never enter the operational tracing/file sink. This transport is not present in production artifacts and never replaces the production email sender after a delivery failure.prompt, max_age, and anything else /oauth/authorizedoesn't explicitly recognize — are rejected outright rather than ignored. Read Known incompatibilities with common OIDC libraries before you wire up an off-the-shelf client; it will save you a debugging session.Discovery
Point your OIDC client at the discovery document — it lists every endpoint, supported scope, and the signing algorithm, so you rarely hard-code URLs. auth.example.com below is a placeholder: authreads is a single shared API host per environment (tenants are distinguished by client_id, not by subdomain). The real host for your environment — the value that becomes your issuer — is published in Environments & base URLs. The issuerin the document always equals the origin you fetched it from, as OpenID Connect Discovery 1.0 §4.3 requires, so a conformant library needs no override. Resolve every other endpoint from the document rather than constructing URLs by hand, so a future path change doesn't break your integration:
curl https://auth.example.com/.well-known/openid-configuration
# → {
# "issuer": "https://auth.example.com",
# "authorization_endpoint": ".../oauth/authorize",
# "token_endpoint": ".../oauth/token",
# "userinfo_endpoint": ".../oauth/userinfo",
# "revocation_endpoint": ".../oauth/revoke",
# "end_session_endpoint": ".../oauth/logout",
# "backchannel_logout_supported": true,
# "backchannel_logout_session_supported": true,
# "jwks_uri": ".../.well-known/jwks.json",
# "id_token_signing_alg_values_supported": ["EdDSA"],
# "code_challenge_methods_supported": ["S256"],
# "token_endpoint_auth_methods_supported": ["none","client_secret_basic",
# "client_secret_post"],
# "grant_types_supported": ["authorization_code","refresh_token",
# "client_credentials",
# "urn:ietf:params:oauth:grant-type:token-exchange"],
# "scopes_supported": ["openid","profile","email","memberships",
# "offline_access","account:sessions"]
# }Register an OIDC application
OIDC clients are registered by hand in the admin dashboard — there is no dynamic/self-service registration endpoint. One active app can hold at most one active OIDC client; revoke and re-register if you need to start over.
- Open the credential formIn the admin dashboard, go to API and credentials → Create credential → Sign in with Authreads, then pick the application this client belongs to. Any active app is eligible — unlike a login-password credential, the app does not need This app signs users in enabled.
- Choose the client type — this is permanentConfidential (server application) or Public (browser or native application, PKCE only, no secret). This cannot be changed after creation; register a new client if you got it wrong.
- Add redirect and logout URIsOne exact URI per line. Every URI, in both fields, must be:
- An absolute HTTPS URL, except that native authorization and post-logout callbacks may use plain HTTP on
localhost,127.0.0.1, or[::1]. - Free of a userinfo/password component and free of a
#fragment. - Free of wildcards — matching is exact-string, not prefix or pattern. For the three native loopback hosts only, the callback port may differ on both authorization and logout so the OS can assign a free port.
- At most 2048 bytes, and not duplicated within the same list (20 URIs max per list).
backchannel_logout_uriif Authreads must notify your server when a session ends. Private, loopback, link-local, credential-bearing, wildcard, and fragment URLs are rejected; DNS is checked again on every delivery. To change them on an existing client, open API and credentials in the admin console, find the Sign in with Authreads row for your application, and choose Manage OIDC client. - An absolute HTTPS URL, except that native authorization and post-logout callbacks may use plain HTTP on
- Pick scopes
openidis required and always on. Choose fromprofile,email,memberships,offline_access, andaccount:sessions— any scope outside this set is rejected. Scopes can be widened or narrowed later; a client can only ever request what it's been granted here. - Save the client secret now — it is shown exactly onceConfidential clients get a
client_idand aclient_secret, shown once in a reveal dialog; authreads stores only a digest and cannot show it again. Public clients get only aclient_id— no secret is ever issued. Rotating a confidential client's secret invalidates the previous one immediately, with no overlap window.
http://127.0.0.1:3000/callback and http://127.0.0.1:3000/signed-out, bind the native listener to port 0, and send the OS-assigned port at runtime. Authreads ignores only that loopback port; scheme, host, path, and query still have to match. Every non-loopback URI requires HTTPS and an exact match, including its port.Verified email claim
Request the email scope to receive both email and the standard email_verified boolean in the ID token. Without that scope, authreads omits both claims entirely; it never emits a guessed value.
email_verified: true.authreads_otp, authreads_token, or tenant_asserted.Three tokens, three jobs
The token response can contain three tokens — using the wrong one is the most common OIDC bug:
| Token | What it's for | Where it goes |
|---|---|---|
| id_token | Proof of who the user is, for your app/session. | Your app only — never send it to an API. |
| access_token | The ticket to call an API. | Sent as Bearer to APIs; your backend verifies it. |
| refresh_token | Get new access tokens without re-login. | Backend only — keep it secret. |
idle_timeout_minutes from its signed https://authreads.com/claims/session_policy claim. This claim is emitted on both authorization-code and refresh-token ID tokens; do not infer activity from a token refresh. Direct/BFF integrations instead use the enforced x-session-activity contract.Scopes
| Scope | Grants |
|---|---|
| openid | Required for OIDC — returns an id_token. |
| profile | Basic profile claims. |
| The user's email claim. | |
| memberships | The user's org memberships and roles. |
| offline_access | Issues a refresh_token for long-lived access. |
| account:sessions | Self-service session management (see below). |
Logout & token revocation
“I logged out but the token still works” comes from treating logout as one action when it's actually three independent layers:
- End the hosted session — redirect to
GET /oauth/logout(theend_session_endpoint). The next/authorizewill prompt for login again. - Revoke the token —
POST /oauth/revokeinvalidates a refresh (or access) token immediately, cutting off renewal. - The access token's TTL — an already-issued access token is self-contained and stays valid until it expires, even after logout. That's why access-token lifetimes are short: you rely on the short TTL plus refresh-token revocation, not on “instant” access-token death.
If you send logout state, Authreads echoes it unchanged onto the already-validated post-logout redirect. Use a random correlation value to verify the response belongs to your logout request. It is not a secret channel: do not put tokens, personal data, or application state inside it.
POST /oauth/account/sessions/revoke-others, below) so no device keeps renewing. Note that step-up re-verification refreshes a session — it does not end one.Back-channel logout
Implement OpenID Connect Back-Channel Logout 1.0 at the HTTPS URI registered on your client. Authreads sends a form-encoded logout_tokenoutside the browser when an OIDC session is revoked or reaches absolute expiry. Delivery is asynchronous: temporary failures are retried at 1, 5, 30, and 120 minutes, then shown as a persistent failure in the client settings. The user's own logout never waits for your endpoint.
POST https://app.acme.com/your-backchannel-logout-uri
Content-Type: application/x-www-form-urlencoded
logout_token=<signed logout+jwt>backchannel_logout_uri. Authreads intentionally rejectslocalhost, private/link-local IPs, redirects, and non-HTTPS endpoints in every environment because this server-to-server callback is an SSRF boundary. Keep tunnel access logs free of the form body: logout_token is a short-lived signed credential.- Verify the EdDSA signature and
kidwith the discovery JWKS. - Require your issuer and exact client ID in
iss/aud, and validateiat/exp. - Require the standard back-channel logout
eventsmember and reject any token containingnonce. - Deduplicate on
jti, then terminate the session identified bysid. Receiving an already-ended session is success. - Return
200or204; do not redirect.
Self-service session management
With the account:sessionsscope a signed-in user can see and revoke their own sessions — useful for a “devices & sessions” screen in your product:
GET /oauth/account/sessions— list the user's active sessions.POST /oauth/account/sessions/{session_id}/revoke— sign out one session.POST /oauth/account/sessions/revoke-others— sign out everywhere else.
/oauth/account/password/* (forgot, reset, resend, and a status check) so users can recover access without leaving the authreads login experience.prompt=login or max_age would do on a generic OIDC provider)? /oauth/authorize rejects both — use the session re-verification API described in Sessions & tokens instead, or see Known incompatibilities with common OIDC libraries.