← Auth & IdP overview

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

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.