Authentication is OIDC via Auth.js (@auth/sveltekit). Any standard OpenID
Connect provider works (Keycloak, Auth0, Okta, Azure AD, Google, your corporate
SSO). This doc covers the provider-agnostic setup, then a concrete Keycloak
walkthrough for local testing.
- The provider id is
oidc, so the callback (redirect) URI is{ORIGIN}/auth/callback/oidc. - The flow is authorization code with PKCE against a confidential client.
- Sessions are stored in an encrypted JWT cookie signed with
AUTH_SECRET; there is no server-side session store. - The user's
subclaim is the stable identity. Private teams are keyed by it, and admin matching uses it (oremail). Theemailclaim should be present for the admin allowlist and the audit log to be readable.
-
Register a confidential client with your provider.
-
Set its redirect URI to
{ORIGIN}/auth/callback/oidc(for examplehttps://team-health.example.com/auth/callback/oidc). -
Make sure the issued ID token includes the
subandemailclaims. -
Set these environment variables and leave
AUTH_DISABLEDunset:OIDC_ISSUER: the provider's issuer URL (its.well-known/openid-configurationlives under it).OIDC_CLIENT_ID,OIDC_CLIENT_SECRET: the client credentials.AUTH_SECRET: any 32+ character random string that signs the session cookie.ORIGIN: the app's public URL, used to build the callback.
-
Grant admins by listing their emails or subjects in
ADMINS.
In a production build, a missing OIDC_ISSUER does not disable auth: the app fails
closed and stays locked until SSO is configured. The AUTH_DISABLED=true bypass is
for development only.
A concrete end-to-end example (sign-in, identity, per-user team persistence,
sign-out). In dev with no OIDC_ISSUER the app auto-bypasses auth; these steps
exercise the real flow.
docker run -d --name keycloak -p 8080:8080 \
-e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:24.0 start-devThe client is confidential (authorization code plus PKCE) and its redirect URI
must match the app's callback. Below assumes the app on http://localhost:5170.
kc() { docker exec keycloak /opt/keycloak/bin/kcadm.sh "$@"; }
kc config credentials --server http://localhost:8080 --realm master \
--user admin --password admin
kc create realms -s realm=teamhealth -s enabled=true
kc create clients -r teamhealth \
-s clientId=team-health -s enabled=true -s protocol=openid-connect \
-s publicClient=false -s standardFlowEnabled=true \
-s 'redirectUris=["http://localhost:5170/auth/callback/oidc"]' \
-s 'webOrigins=["http://localhost:5170"]'
# Copy the "value" from this into OIDC_CLIENT_SECRET:
CID=$(kc get clients -r teamhealth -q clientId=team-health --fields id --format csv --noquotes | tail -1)
kc get clients/$CID/client-secret -r teamhealth
kc create users -r teamhealth -s username=alice -s enabled=true \
-s email=alice@example.com -s emailVerified=true -s firstName=Alice -s lastName=Tester
kc set-password -r teamhealth --username alice --new-password alice123AUTH_DISABLED must be unset (a non-true value is fine), and a fixed port is
needed so the redirect URI matches.
AUTH_DISABLED= \
OIDC_ISSUER=http://localhost:8080/realms/teamhealth \
OIDC_CLIENT_ID=team-health \
OIDC_CLIENT_SECRET=<secret from step 2> \
AUTH_SECRET=<any 32+ char string> \
ORIGIN=http://localhost:5170 \
pnpm dev --port 5170 --strictPortOpen http://localhost:5170 and sign in as alice / alice123. Custom teams are
then persisted per user (keyed by the OIDC subject) when DATABASE_URL is set.
/auth/csrfreturns 404 by design: Auth.js v5 uses SvelteKit's origin-based CSRF, so the[auth][warn][csrf-disabled]log line is expected, not an error.