OIDC provider quick-start
assay-engine v0.2.0 is a conformant OpenID Connect provider — drop-in Ory Hydra
replacement. Authorization-code + PKCE, refresh tokens, RFC 7009 revoke, RFC 7662
introspect, JWKS rotation, back-channel logout. Register a client and your existing
OIDC-aware app (Immich, Grafana, ArgoCD, Nextcloud, Outline, Kanboard, …) talks to assay
instead of Hydra/Keycloak/Auth0.
1. Boot the engine with auth on
# engine.toml
auto_enable_modules = ["auth"]
[server]
bind_addr = "0.0.0.0:3000"
public_url = "https://auth.example.com" # MUST be the public HTTPS URL
[backend]
type = "postgres"
url = "postgres://postgres:postgres@localhost/assay"
[auth]
issuer = "https://auth.example.com/auth"
admin_api_keys = ["sk_admin_replace_me"]
[auth.oidc_provider]
enabled = true
assay-engine serve --config engine.toml
Verify the discovery doc is live:
curl https://auth.example.com/.well-known/openid-configuration | jq .
# {
# "issuer": "https://auth.example.com/auth",
# "authorization_endpoint": "https://auth.example.com/auth/authorize",
# "token_endpoint": "https://auth.example.com/auth/token",
# "userinfo_endpoint": "https://auth.example.com/auth/userinfo",
# "jwks_uri": "https://auth.example.com/.well-known/jwks.json",
# "response_types_supported": ["code"],
# "grant_types_supported": ["authorization_code", "refresh_token"],
# "code_challenge_methods_supported": ["S256"],
# ...
# }
2. Register an OIDC client
Three ways to do this — pick whichever fits your workflow:
via the dashboard
Open https://auth.example.com/auth/console, log in as admin, navigate to
OIDC clients, click New client. The dashboard shows the
generated client_secret exactly once — copy it before closing.
via the admin HTTP API
curl -X POST https://auth.example.com/auth/admin/auth/oidc-clients \
-H "Authorization: Bearer sk_admin_replace_me" \
-H "Content-Type: application/json" \
-d '{
"name": "Immich",
"redirect_uris": ["https://photos.example.com/auth/login"],
"default_scopes": ["openid", "email", "profile"],
"token_endpoint_auth_method": "client_secret_basic",
"pkce_required": true
}'
# { "client_id": "...", "client_secret": "shown ONCE — store now", "name": "Immich", ... }
via the assay.auth Lua wrapper
local auth = require("assay.auth")
local c = auth.client({
engine_url = "https://auth.example.com",
api_key = env.get("ASSAY_ADMIN_KEY"),
})
local result = c.oidc_clients:create({
name = "Immich",
redirect_uris = { "https://photos.example.com/auth/login" },
default_scopes = { "openid", "email", "profile" },
})
print(result.client_id, result.client_secret) -- secret returned ONCE
3. Configure the consumer (Immich example)
# Immich .env or k8s ConfigMap
OAUTH_AUTO_REGISTER=true
OAUTH_AUTO_LAUNCH=true
OAUTH_BUTTON_TEXT="Sign in with Acme"
OAUTH_ISSUER_URL=https://auth.example.com/auth
OAUTH_CLIENT_ID=<the client_id from step 2>
OAUTH_CLIENT_SECRET=<the client_secret from step 2>
OAUTH_SCOPE="openid email profile"
Restart Immich. The login screen now shows "Sign in with Acme"; clicking it round-trips
through assay-engine's /auth/authorize, your password (or passkey) login, the
consent screen, and back to Immich's callback with a freshly minted id_token signed by the
JWKS at /.well-known/jwks.json.
4. Operating concerns
- JWKS rotation. The provider rotates signing keys on a schedule (see
auth.jwks_keys). Old kids stay published until pruned, so existing tokens verify until expiry. Trigger a manual rotation from the dashboard's JWKS pane or via the admin HTTP API. - Back-channel logout. Set
backchannel_logout_urion the client and assay will POST a Logout Token there when the user logs out of any session sharing the same SID. - Token revocation. RFC 7009 —
POST /auth/oauth2/revokewith a refresh or access token. The token is markedrevoked = TRUEinauth.oidc_refresh_tokens. - Token introspection. RFC 7662 —
POST /auth/oauth2/introspectfor resource servers that need to validate tokens server-side. - PKCE-required by default. All new clients enforce PKCE
(
S256). Setpkce_required = falseon creation if you absolutely must support a legacy non-PKCE consumer.
5. Federated SSO (assay as RP)
Want assay-engine to also let users sign in with Google/Apple/GitHub? Register an upstream provider from the dashboard's Upstream providers pane, or via API:
curl -X POST https://auth.example.com/auth/admin/auth/upstream \
-H "Authorization: Bearer sk_admin_replace_me" \
-H "Content-Type: application/json" \
-d '{
"slug": "google",
"display_name": "Google",
"issuer": "https://accounts.google.com",
"client_id": "123-...apps.googleusercontent.com",
"client_secret": "GOCSPX-...",
"enabled": true
}'
Now the login screen offers a "Sign in with Google" button alongside password + passkey.
assay-engine handles the federation handshake, links the upstream (provider, sub)
tuple to a local auth.users row, and continues the local session as if the
user had logged in directly.
See Compare vs Ory for how this all stacks up vs Hydra.