6. Auth and users
Riverbase consumes identity. It validates tokens and sessions, resolves a principal and an
active profile, and puts that identity into EngineContext. It is not a user directory. Account
creation, passwords, and role assignment live in your IdP (Keycloak or any OIDC provider) or your
own identity service.
Turn it on
The auth feature of riverbase_http is on by default. Configure a provider under
[riverbase.auth], or set RIVERBASE_AUTH_* variables.
Local development: MockAuth
MockAuth is a local OAuth2 identity provider with a user picker, plus an Authorization: MockAuth-…
header for tests. It never injects a default user into anonymous requests.
[riverbase.auth]
auth_provider = "MockAuth"
oauth2_client_id = "todo"
application_secret_key = "change-me-in-production"
mock_sub = "alice"
mock_realm_access = ["todo-admin"]
[[riverbase.auth.mock_users]]
sub = "bob"
username = "bob"
roles = ["todo-viewer"]
The browser flow matches Keycloak: /auth/sign-in redirects to the MockAuth picker, which returns
code and state to /auth/callback, and the session is written at the callback. mock_* keys
and mock_users define who appears in the picker.
API tests skip the browser and send a header:
Authorization: MockAuth-<base64url(JSON claims)>
The claims need a non-empty sub. Roles go in a top-level roles array or Keycloak-style
realm_access.roles. Other schemes, such as Bearer … while MockAuth is active, return
400 AUT-172. A request with no header and no session is anonymous.
Production: Keycloak or another OIDC provider
[riverbase.auth]
auth_provider = "Keycloak"
issuer = "https://keycloak.example.com/realms/myrealm"
audience = "my-client"
accepted_azp = ["my-client"]
keycloak_base_url = "https://keycloak.example.com"
keycloak_realm = "myrealm"
oauth2_client_id = "my-client"
# oauth2_client_secret and application_secret_key come from the environment:
# RIVERBASE_AUTH_OAUTH2_CLIENT_SECRET, …
Bearer JWTs are validated against the issuer's JWKS, with audience and the authorized party
(azp) checked. Browser login uses the OAuth2 routes.
The full list of auth keys, including cookie, session, and redirect settings, is in the Configuration reference.
Mount the auth routes
app.mount_domain(domain).await?;
app.mount_auth_routes().await?; // /api/auth/* (JSON)
app.mount_auth().await?; // /auth/* browser OAuth2 flow
app.serve().await?;
mount_auth_routes adds:
| Route | Purpose |
|---|---|
GET /api/auth/info | The resolved authorization context for the caller |
GET /api/auth/home | Browser bounce: with a session go to the SPA, otherwise start sign-in |
GET /api/auth/sign-in | Always start the IdP flow |
GET /api/auth/profiles | Profiles available to the signed-in user |
POST /api/auth/switch-profile/{profile_id} | Change the active profile (session and database) |
If the provider is None and RIVERBASE_AUTH_DISABLED=1 is not set, mount_auth_routes fails at
startup. That is deliberate: a misconfigured service should not serve silent 404s.
Profiles
A profile is the identity a user acts as: usually a membership in one organization, which also
decides the tenant. mount_auth_routes uses a Keycloak-backed provider when Keycloak is configured,
and an empty provider otherwise. To list and switch profiles from your own identity store, implement
AuthProfileProvider and pass it in:
app.mount_auth_routes_with(Arc::new(MyProfileProvider::new(pool.clone()))).await?;
Which profile is active
| Mode | Resolution order |
|---|---|
| Bearer token | X-Profile header → JWT profile_id → stored current profile → first active profile |
| Session | X-Profile header → session active_profile_id → stored current profile → first active profile |
Stateless API clients pin a profile per request:
X-Profile: 550e8400-e29b-41d4-a716-446655440000
Authorization: Bearer eyJ…
From request to engine
- Middleware validates the bearer token or session and builds a
Principal. - The route handler builds an
EngineContextwith actor, profile, tenant, roles, and server-resolved claims. - Command and query engines receive that context on every call.
Clients never set claims directly. Authentication answers who is calling. Whether they may run a given command, and on which rows, is chapter 7.
Verify locally
# MockAuth header, no session
CLAIMS=$(printf '%s' '{"sub":"alice","roles":["todo-admin"]}' | basenc --base64url | tr -d '=')
curl -s -H "Authorization: MockAuth-$CLAIMS" http://127.0.0.1:8080/api/auth/info
Without the header, /api/auth/info returns 401.
Checklist
-
[riverbase.auth] auth_providerset (MockAuthlocally) -
mount_auth_routes()(andmount_auth()for browser login) called beforeserve -
GET /api/auth/inforeturns the expected principal - An
AuthProfileProviderfor environments with real profiles
Next: 7. Permissions.