OIDC SSO — bring your issuer.
On every plan, on day one. Hand us an OIDC issuer URL and we discover the endpoints, fetch the JWKS, verify theid_token, optionally gate by allowed email domain. Apple, Microsoft, Okta, Auth0, your own — all the same flow.
What you configure
In the per-app Providers tab, add an OIDC provider with:
- Issuer URL — e.g.
https://accounts.google.com,https://login.microsoftonline.com/<tenant>/v2.0,https://<your-domain>.okta.com. - Client ID + secret — from your IdP's app registration.
- Allowed domain (optional) — only emails matching this domain can sign in via this provider. Use this for B2B SSO where only
@acme.comusers should land in the Acme app. - Scope override (optional) — default is
openid email profile; some IdPs need more.
On save we hit <issuer>/.well-known/openid-configuration and cache the authorization, token, userinfo, and JWKS endpoints in tillauth_app_oauth. JWKS is re-fetched lazily — keys are revalidated on a kid miss, not on every signin.
The flow
- Client calls
GET /v1/oidc/start?redirect_uri=…. We mint a PKCE pair + a state JWT, redirect to the issuer's authorization endpoint. - Issuer authenticates the user (their UI, their MFA, their device trust), bounces back to
/v1/oidc/callback?code=…&state=…. - We verify the state JWT (bound to IP
/24, fresh), exchange the code for tokens at the token endpoint with PKCE. - We verify the
id_token:issmust equal the configured issuer.audmust include the configured client ID.- Signature must verify against a key in the cached JWKS (refresh on
kidmiss). expnot past,iatnot unreasonably future,noncematches.
- If
allowed_domainis set, theemailclaim's domain must match — case-insensitive. Mismatch returns 403 with auditsignin.oauth.error+ reasondomain_not_allowed. - Look up
(app_id, provider='oidc', subject=id_token.sub)intillauth_identities. Found → session for the linked user. Not found + verified email matches an existing user → link onto them. Else → JIT-create the user with the claims (email, name).
Allowed-domain gate
This is the killer feature for B2B. You stand up TillAuth for the Acme-Admin app, point it at Acme's Okta, setallowed_domain = "acme.com". Now:
- An
@acme.comuser who signs in with Okta → user created (or linked), session minted. - A
@gmail.comuser who somehow gets through Okta → rejected at the TillAuth boundary. No account is created.
What you can do once it's wired
- Restrict signup methods: turn off password + magic-link per-app so the only way in is your SSO.
- JIT provisioning: the first time an Okta user signs in for a given app, the user row is created — no manual seeding.
- Deprovision via session revoke: when someone leaves Acme, revoke their sessions via
DELETE /v1/admin/apps/<appId>/users/<userId>/sessions— they can't sign in again until Okta removes them.
What we don't do
SAML. TillAuth is OIDC-only. Most modern IdPs (Okta, Azure AD, Google Workspace, Auth0, Keycloak) speak OIDC; if the only path you have is SAML, you'll need to front it with an OIDC bridge first.
Next: webhooks to react to signin events ·audit log for the record of every flow.