Any app, via OIDC¶
Omxus is a standard OpenID Connect provider. If your framework speaks OIDC, and almost all of them do, you write no auth code at all: point it at the discovery document and register your redirect URI.
Discovery¶
| field | value |
|---|---|
| issuer | https://auth.omxus.com |
| authorization_endpoint | https://auth.omxus.com/oidc/authorize |
| token_endpoint | https://auth.omxus.com/oidc/token |
| userinfo_endpoint | https://auth.omxus.com/oidc/userinfo |
| jwks_uri | https://auth.omxus.com/oidc/jwks |
| end_session_endpoint | https://auth.omxus.com/oidc/logout |
| registration_endpoint | https://auth.omxus.com/oidc/register |
| revocation_endpoint | https://auth.omxus.com/oauth/revoke |
| introspection_endpoint | https://auth.omxus.com/oauth/introspect |
| scopes | openid, profile, email, phone, offline_access |
| grants | authorization_code, refresh_token (PKCE S256 only) |
| client auth | client_secret_basic, client_secret_post, none |
| response mode | query |
| subject | pairwise |
| id_token signing | ES256 |
The document and the JWKS are public, sent with CORS, and cached for five minutes.
Two doors, one protocol¶
Every client is registered with a door (see Registering a site). The protocol is the same on both; what comes back differs.
| Omxus door | Standard door | |
|---|---|---|
| Who signs in | an Omxus identity (passkey, phrase, three inputs) | an ordinary account (email or phone, password, code, passkey) |
| Scopes answered | openid, profile |
openid, profile, email, phone |
| Refresh token | none | on every code exchange |
| Access token | 1 hour | 15 minutes; refresh token 30 days, rotates |
| Loopback redirect, any port | no | yes (native apps, RFC 8252) |
offline_access is accepted on the standard door but not needed: a refresh
token comes back either way.
The flow¶
Authorization code with PKCE. PKCE is required for every client, public or confidential.
- Redirect to
/oidc/authorizewithclient_id,redirect_uri,response_type=code,scope=openid profile,state,nonce,code_challengeandcode_challenge_method=S256. - The person signs in on the provider's page. On the standard door, someone already signed in to Omxus sees "Choose an account" and presses once; after that first press, this site gets them back with no press at all.
- You get
?code=…&state=…at your redirect URI. The code lasts 60 seconds. - POST the code, the same
redirect_uriand thecode_verifierto/oidc/token, form-encoded, with your client secret if you have one. - Verify the
id_tokenagainst/oidc/jwks:iss,aud(yourclient_id),exp, andnonce(the one you sent).
The token answer:
{
"access_token": "…",
"token_type": "Bearer",
"expires_in": 3600,
"id_token": "eyJ…",
"scope": "openid profile"
}
Standard-door clients also get refresh_token, and expires_in is 900.
Refresh with grant_type=refresh_token; each refresh token works once and
the answer carries the next one. Replaying a spent one after 30 seconds ends
the whole grant.
Scopes and claims¶
| scope | claims |
|---|---|
openid |
sub, iss, aud, iat, exp, auth_time, nonce |
profile |
Omxus door: omxus_address. Standard door: preferred_username, name, picture, profile |
email |
email, email_verified (standard door) |
phone |
phone_number, phone_number_verified (standard door) |
Standard-door ID tokens also carry amr (pwd, hwk, otp or swk), and
legacy_id and roles for people you imported
(see Migrating an existing login). Without profile, an
Omxus-door token holds no address at all. sub is pairwise: see
How identity is derived.
/oidc/userinfo takes the access token as Authorization: Bearer and
returns the same sub the ID token carried.
prompt¶
| value | Omxus door | Standard door |
|---|---|---|
| none | login_required if no Omxus session, else a code |
a code if this site was chosen before; login_required, consent_required (first visit) or interaction_required (needs a username for this site) otherwise |
| login | ignored | always shows the sign-in form |
| select_account | ignored | shows the chooser, even on first-party sites |
Errors from prompt=none come back to your redirect URI as ?error=…&state=….
Signed in everywhere uses this for silent sign-in.
Errors¶
A bad client_id or an unregistered redirect_uri is shown to the person
as a page (HTTP 400) and never redirected: there is nowhere trustworthy to
send them. Every other authorize error redirects back with error,
error_description and your state.
| where | error | when |
|---|---|---|
| authorize | unsupported_response_type |
response_type is not code |
| authorize | invalid_scope |
no openid in scope |
| authorize | invalid_request |
no code_challenge, or method not S256 |
| authorize | unauthorized_client |
sector_identifier_uri not yet approved |
| token | invalid_client (401) |
unknown client, missing or wrong secret |
| token | invalid_grant |
code used, expired, for another client, wrong redirect_uri, or PKCE fails |
| token | unsupported_grant_type |
anything but the two grants |
| userinfo | invalid_token (401) |
missing, unknown or expired token |
Signing out¶
Send the person to /oidc/logout with client_id (or id_token_hint),
post_logout_redirect_uri and optionally state. The provider ends its
session and redirects only to a URI registered for that client, or to the
origin of one of its redirect URIs, with state added. Anything else shows
"Signed out." and stops there.
On the standard door, send id_token_hint: only a hint whose sub matches
the signed-in account ends the provider session and this site's grants.
With client_id alone you get the redirect and nothing is ended.
To end one grant without signing the person out of Omxus, revoke the token
at /oauth/revoke (RFC 7009).
Verified behaviour¶
These are covered by the auth test suite (server/oidc.test.js, 28 checks;
server/accounts/oidc-standard.test.mjs, 114), all passing on 30 Oct 2026:
- An
id_tokenverifies against the published JWKS; a tampered one fails. nonceis echoed,audis your client.subis pairwise: two sites get different values for the same person, the same site gets the same value every time.- A code cannot be redeemed twice, by another client, with another
redirect_uri, or without the PKCE verifier. - An unregistered
redirect_uriis shown to the person, never redirected to. - Refresh tokens rotate; a replayed one kills the grant and is audited.
Register the exact redirect URI
Matching is exact. https://site.com/callback and
https://site.com/callback/ are different URIs, and the second will be
refused. This is deliberate: loose matching is how open redirectors become
account takeovers.
Tailscale and other private networks¶
The provider is public, so a tailnet reaches it. The catch is your redirect
URI: it must be https, http on localhost, 127.0.0.1 or [::1], or a
reverse-DNS app scheme. http://machine:3000 is refused at registration.
Turn on Tailscale HTTPS so your app is at https://machine.tailnet.ts.net,
then register that exact URL.