TILLAUTH · FLOWS

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.com users 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

  1. Client calls GET /v1/oidc/start?redirect_uri=…. We mint a PKCE pair + a state JWT, redirect to the issuer's authorization endpoint.
  2. Issuer authenticates the user (their UI, their MFA, their device trust), bounces back to /v1/oidc/callback?code=…&state=….
  3. We verify the state JWT (bound to IP /24, fresh), exchange the code for tokens at the token endpoint with PKCE.
  4. We verify the id_token:
    • iss must equal the configured issuer.
    • aud must include the configured client ID.
    • Signature must verify against a key in the cached JWKS (refresh on kid miss).
    • exp not past, iat not unreasonably future, nonce matches.
  5. If allowed_domain is set, the email claim's domain must match — case-insensitive. Mismatch returns 403 with auditsignin.oauth.error + reason domain_not_allowed.
  6. Look up (app_id, provider='oidc', subject=id_token.sub) in tillauth_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.com user who signs in with Okta → user created (or linked), session minted.
  • A @gmail.com user 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.