Skip to main content

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.

service/riverbase.toml
[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​

service/riverbase.toml
[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​

service/src/main.rs
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:

RoutePurpose
GET /api/auth/infoThe resolved authorization context for the caller
GET /api/auth/homeBrowser bounce: with a session go to the SPA, otherwise start sign-in
GET /api/auth/sign-inAlways start the IdP flow
GET /api/auth/profilesProfiles 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​

ModeResolution order
Bearer tokenX-Profile header → JWT profile_id → stored current profile → first active profile
SessionX-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​

  1. Middleware validates the bearer token or session and builds a Principal.
  2. The route handler builds an EngineContext with actor, profile, tenant, roles, and server-resolved claims.
  3. 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_provider set (MockAuth locally)
  • mount_auth_routes() (and mount_auth() for browser login) called before serve
  • GET /api/auth/info returns the expected principal
  • An AuthProfileProvider for environments with real profiles

Next: 7. Permissions.