Last open item from the roadmap + post-1.0 list. Run against the live Clerk instance closing-seasnail-39.clerk.accounts.dev. - Backend (crates/cm-auth/tests/live_clerk.rs, CM_LIVE_CLERK=1): pulls REAL discovery + JWKS from the live instance, mints a REAL session JWT via Clerk's Backend API (create user -> open session -> session token), and runs it through AuthService::authenticate — verify + JIT provision (keyed on the real sub), duplicate-subject suppression, tamper rejection against the live JWKS. Decodes the instance domain from the publishable key; cleans up the test user after. PASSING - Frontend: built with AUTH_MODE=clerk + real keys, next start serves Clerk's <SignIn /> at /login wired to the instance (instance domain + data-clerk attributes present in the HTML). Both halves confirmed end to end against production Clerk - docs/clerk.md: documented the smoke procedure for both halves 166 Rust tests (+6 live, key-gated). Keys used via env only, never stored — rotate them (they passed through chat). Co-Authored-By: Claude Fable 5 <[email protected]>
3.7 KiB
Using Clerk for authentication
Clawmates's cloud target can use Clerk as its identity provider. A Clerk instance is an OIDC issuer, so the backend verifies Clerk session JWTs directly against the instance JWKS — no extra service, no session table writes for SSO users, and Clerk's dashboard takes over user management, MFA, and organization invitations.
Air-gapped installs cannot use Clerk (it is a hosted service). The
local auth mode remains the appliance default; clerk is a cloud-only
alternative to generic oidc.
1. Clerk dashboard
-
Create an application; note the Frontend API URL (
https://<slug>.clerk.accounts.dev). -
Enable Organizations — Clerk's
org:admin/org:memberroles map to ClawmatesOwner/Member. -
Under Sessions → Customize session token, add the claims Clawmates reads:
{ "email": "{{user.primary_email_address}}", "role": "{{org.role}}" }
2. Backend configuration
[auth]
mode = "clerk"
issuer_url = "https://<slug>.clerk.accounts.dev"
Or via Helm: --set auth.mode=clerk --set auth.issuerUrl=https://<slug>.clerk.accounts.dev.
On boot the server pins the issuer, loads its JWKS (refreshing on key
rotation), and verifies every Authorization: Bearer <session JWT> with a
5-second clock-skew allowance. Users are JIT-provisioned on first sight,
keyed by the stable sub claim (users.auth_subject); an existing
local-auth user with the same email is linked rather than duplicated, and
the role tracks the Clerk org role on every request.
3. Frontend wiring (built in)
The frontend ships with the integration. Set three runtime env vars on the frontend container — the SAME image serves local and Clerk deployments, nothing is baked at build time:
AUTH_MODE=clerk
CLERK_PUBLISHABLE_KEY=pk_live_…
CLERK_SECRET_KEY=sk_live_…
What flips on: the root layout wraps the tree in <ClerkProvider>
(lazily imported — local mode never loads the SDK), /login renders
Clerk's <SignIn />, the middleware runs clerkMiddleware(), and every
server-side API call resolves its bearer through Clerk's getToken()
instead of the local session cookie (src/lib/auth/bearer.ts is the
single dispatch point). Clerk session JWTs live ~60s and refresh
transparently; the backend verifies each one against the instance JWKS.
What is verified in CI
crates/cm-auth/tests/jwt_auth.rs and crates/cm-api/tests/clerk_api.rs
exercise the full verification path with real RSA keys against a live
local issuer publishing real discovery + JWKS documents: JIT provisioning
and role mapping, duplicate-subject suppression, expired tokens, tokens
signed by the wrong key, foreign issuers, and the end-to-end
Authorization: Bearer round trip into a protected endpoint.
Deployment smoke (real instance)
crates/cm-auth/tests/live_clerk.rs validates the production path against
a live Clerk instance: it pulls real discovery + JWKS, uses Clerk's
Backend API to create a user, open a session, and mint a real session JWT,
then runs that token through AuthService::authenticate — proving
verification, JIT provisioning, duplicate-subject suppression, and
tamper rejection against Clerk's actual keys. It cleans up the user
afterward. Run it with your instance keys (key-gated, never in the default
suite):
CM_LIVE_CLERK=1 \
CLERK_SECRET_KEY=sk_test_… \
CLERK_PUBLISHABLE_KEY=pk_test_… \
cargo test -p cm-auth --test live_clerk
The frontend half is smoked by building with AUTH_MODE=clerk and the
same keys and confirming /login serves Clerk's <SignIn /> wired to the
instance — npx next start then curl /login shows the instance domain
and data-clerk attributes.