Authentication
How OIDC authentication works with FerrisKey in the DX SaaS Template
Overview
The template uses FerrisKey as the identity provider, integrated via OIDC (authorization code flow with PKCE) and a custom login UI that drives FerrisKey's REST API directly. Authentication is session-based using tower-sessions with PostgreSQL as the session store.
FerrisKey is an open-source, Rust-native IdP with WebAuthn/passkey support. The template's custom login page replaces FerrisKey's built-in UI so you control the UX end-to-end.
Auth Flow
The login page supports three credentials, auto-detected from the user's account state:
User enters email
The login page POSTs to /auth/session/start. The server looks up the user in FerrisKey by email and inspects their credentials.
Branch on credentials
- Has passkey → server bootstraps a FerrisKey auth-flow session and returns WebAuthn request options. The browser invokes
navigator.credentials.get()and POSTs the assertion back to/auth/session/passkey/verify.- Has password → page shows a password input that POSTs to
/auth/session/password/verify. - No credentials / new user → CAPTCHA-gated email-OTP flow: a 6-digit code is sent via SMTP and verified at
/auth/session/otp/verify.
- Has password → page shows a password input that POSTs to
Token exchange
On Success, the server exchanges the OIDC code for tokens at FerrisKey's /protocol/openid-connect/token endpoint using the PKCE verifier. The id_token is validated against FerrisKey's JWKS (cached, refreshed on key rotation).
Session cookie is set
The server records the user via AuthUserStore::lookup_or_create_user and stores the session in PostgreSQL. The signed cookie is HTTP-only and (in production) secure + SameSite=Lax.
Client receives auth state
The login page bumps UserDataRefreshTrigger, which re-runs /api/me. UserAuthState transitions to Authenticated and the user is navigated to the redirect URL — no full page reload.
Key Components
Server Side
The auth crate defines two traits that the main app implements:
// AuthUserStore — find or create users, run TOS / post-login redirect logic
// AuthEmailSender — deliver OTP codes via SMTP
UserSession Extractor
Server functions can require authentication by adding a session parameter:
async The UserSession extractor reads the session from the cookie. If the session is missing or invalid, the server function returns an error.
Client Side
The App component provides auth state via context:
A use_server_future fetches /api/me on load. The UserDataRefreshTrigger signal allows any component to trigger a re-fetch (e.g., after login or profile update).
Protected Routes
The DashboardShell layout checks UserAuthState and redirects unauthenticated users to /login:
Configuration
Set these environment variables for FerrisKey:
| Variable | Description |
|---|---|
FERRISKEY_URL |
FerrisKey base API URL (e.g. http://localhost:3333 or https://ferriskey.example.com/api) |
FERRISKEY_ISSUER_URL |
(Optional) Public OIDC issuer base URL. Falls back to FERRISKEY_URL (with /api stripped) if unset. |
FERRISKEY_REALM |
Realm name configured in FerrisKey (e.g. myapp) |
FERRISKEY_CLIENT_ID |
OIDC client ID registered in the realm |
FERRISKEY_CLIENT_SECRET |
Client secret. Required for the authorization-code exchange and the client_credentials grant used for service-account user lookup/create. |
TRUST_PROXY_HEADERS |
Set to true when running behind a reverse proxy so auth rate limiting trusts X-Forwarded-For. |
The OIDC client must allow authorization_code and client_credentials grant types — the server uses the latter to manage users (lookup, create, mark email verified) without an interactive flow.
Security middleware
All routes mounted by auth_router are wrapped with:
- Rate limiting — 20 requests/minute per client IP via
governor. - CSRF Origin check — POST requests must carry
Origin(orReferer) matchingBASE_URL.
Both are tunable in crates/auth/src/router.rs and rate_limit.rs.