Auth & IdP overview
assay-engine v0.2.0 ships a complete identity provider in the same binary that
runs the Temporal-replacement workflow engine. One process. PG18 or SQLite. Ory + Hydra +
Keto in ~19 MB.
What's in v0.2.0
Every primitive a serious self-hosted IdP needs, packaged into one Rust crate
(assay-auth) and composed into assay-engine:
| Module | Replaces | What it does |
|---|---|---|
session | Ory Kratos (sessions) | Cookie + CSRF + 30-day TTL session manager |
password | Ory Kratos (passwords) | Argon2id PHC strings, peppered hashing |
recovery | Ory Kratos (recovery) | SMTP reset links with hashed, expiring, single-use tokens |
jwt | Hydra (JWT) | RS256 issue/verify with rotated JWKS (active + history) |
oidc (client) | Kratos (federation) | Log in via Google / Apple / GitHub / any OIDC IdP |
oidc_provider | Ory Hydra | Full /authorize, /token, /userinfo, RFC 7009 revoke, RFC 7662 introspect, back-channel logout |
passkey | Kratos (WebAuthn) | webauthn-rs-backed passkey register + auth ceremonies |
zanzibar | Ory Keto / SpiceDB | ReBAC tuples + recursive-CTE walk on PG18 + SQLite |
biscuit | (Ory has nothing) | Datalog-attenuable capability tokens — always-on |
admin | Ory Console (HTTP API) | Cross-cutting users / sessions / Zanzibar / keys / audit endpoints |
Why ditch Ory for assay-engine?
- One static binary. ~19 MB stripped,
FROM scratch-shippable. Replaces a stack of Kratos + Hydra + Keto + Oathkeeper containers (typically 800 MB-1.5 GB compressed across 5+ images). - Backend symmetry. PG18 + SQLite are both first-class. SQLite means a self-hosted single-tenant deployment needs no database server at all — unique vs Ory.
- Biscuit out of the box. Datalog-attenuable capability tokens that callers can scope down further without a server round-trip. Ory has nothing equivalent; this is a real differentiator.
- Workflow + auth share storage. Atomic transactions across
auth.users↔workflow.workflows(cross-schema FKs on PG, both attachments on SQLite). Splitting Ory + Temporal forces 2-phase commit. - Lua-scriptable. Every auth surface is reachable from the
assay.authLua stdlib module — operators build login, admin, and federation flows in scripts the runtime binary ships with. - Module enablement is runtime, not build-time. Compile auth in once;
flip
engine.modules.auth.enabledper environment without redeploying.
Storage model
All auth tables live in the auth schema (PG) or attached auth
database (SQLite, default ./data/auth.db). Migrations are idempotent and
version-tracked in the shared engine.migrations table:
| Table | Purpose |
|---|---|
auth.users | Authoritative user records |
auth.sessions | Opaque session ids + CSRF tokens + expiry |
auth.passkeys | WebAuthn credentials per user |
auth.user_upstream | Federated identity links |
auth.audit | Append-only compliance log |
auth.jwks_keys | Rotated JWT signing keys (active + history) |
auth.biscuit_root_keys | Biscuit capability-token root keys |
auth.zanzibar_tuples | ReBAC relation tuples |
auth.zanzibar_namespaces | Per-namespace schema definitions |
auth.oidc_clients | Registered consumer apps (engine = OP) |
auth.upstream_providers | Federated identity providers |
auth.oidc_authorization_codes | Single-use codes from /authorize |
auth.oidc_refresh_tokens | SHA-256-hashed long-lived bearer tokens |
auth.oidc_sessions | SSO session registry (one per id_token) |
auth.oidc_consents | Per-(user, client) consent grants |
auth.oidc_upstream_states | Short-lived federation state |
auth.password_recovery_tokens | SHA-256 token digests and expiry; raw reset tokens are never stored |
Minimum viable config
# engine.toml
auto_enable_modules = ["auth"]
[server]
bind_addr = "0.0.0.0:3000"
public_url = "https://auth.example.com" # OIDC iss + passkey origin
[backend]
type = "postgres"
url = "postgres://postgres:postgres@localhost/assay"
# Or: type = "sqlite", data_dir = "./data"
[auth]
issuer = "https://auth.example.com/auth"
admin_api_keys = ["sk_admin_replace_me"]
[auth.passkey]
rp_id = "auth.example.com"
rp_name = "Acme Identity"
[auth.recovery]
enabled = true
[auth.recovery.smtp]
host = "${SMTP_HOST}"
username = "${SMTP_USERNAME}"
password = "${SMTP_PASSWORD}"
from = "Acme Identity "
[auth.oidc_provider]
enabled = true
# The vault module is on by default and holds every secret under one
# master key. Without unseal material the engine refuses to start rather
# than write that key into the same database as the secrets it protects.
# ASSAY_VAULT_SEAL_KEY is the default source, so no section is needed
# here. To read the key from a file instead — which keeps it out of
# /proc/<pid>/environ and out of core dumps — uncomment:
#
# [vault.sealing]
# source = "file"
# path = "/run/secrets/vault-seal-key" # must be mode 0600
# Any string of at least 32 characters. Not recoverable — back it up.
export ASSAY_VAULT_SEAL_KEY="$(openssl rand -base64 32)"
assay-engine serve --config engine.toml
# /auth/console → admin SPA
# /.well-known/openid-configuration → OIDC discovery (Hydra-equivalent)
# /auth/login, /auth/recovery → password auth and recovery pages
# /auth/passkey/* → passkey flows
# /auth/admin/auth/* → admin HTTP API (api-key gated)
The browser login page always offers local email/password authentication and links to an optional SMTP-backed recovery flow. Enabled OIDC upstream providers appear beneath it as optional alternatives, so a fresh installation does not need an external identity provider before its first user can sign in.
Dashboard panes
When the auth module is enabled in engine.modules, the engine's dashboard SPA
lights up with these panes:
- Users — list, search, view (with passkeys + sessions + upstream links), enable/disable, delete, send password-reset.
- Sessions — global session list, per-user view, revoke single, revoke-all-for-user.
- OIDC clients — CRUD for consumer apps that authenticate against this IdP. Create + view secret-once + rotate-secret + delete.
- OIDC upstream providers — CRUD for federated SSO sources.
- Zanzibar — namespace browser, tuple inspector with filter, check evaluator, expand viewer.
- JWKS / Biscuit keys — list active + history, view public material, trigger rotation.
- Audit log — paginated viewer with filter by actor / action / time range.
Where to next?
- Passkey login flow — code samples + Lua API
- OIDC provider quick-start — register a client + plug into Immich/Grafana/etc.
- JWT pass-through validation — keep your existing IdP, accept upstream JWTs natively (v0.3.2)
- Zanzibar permissions guide — schema, tuples, check API
- Biscuit capability tokens — issue / verify / attenuate
- Compare vs Ory — side-by-side feature matrix