Skip to content

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

curl https://auth.omxus.com/.well-known/openid-configuration
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.

  1. Redirect to /oidc/authorize with client_id, redirect_uri, response_type=code, scope=openid profile, state, nonce, code_challenge and code_challenge_method=S256.
  2. 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.
  3. You get ?code=…&state=… at your redirect URI. The code lasts 60 seconds.
  4. POST the code, the same redirect_uri and the code_verifier to /oidc/token, form-encoded, with your client secret if you have one.
  5. Verify the id_token against /oidc/jwks: iss, aud (your client_id), exp, and nonce (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_token verifies against the published JWKS; a tampered one fails.
  • nonce is echoed, aud is your client.
  • sub is 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_uri is 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.