Federated Login (OAuth2)¶
tuvl supports OAuth2 Authorization Code flow for Google, GitHub, and Microsoft (Entra ID).
Federation providers are configured as YAML files in the federation/ directory and managed
via the Federation page in the tuvl UI.
How It Works¶
sequenceDiagram
participant Browser
participant tuvl
participant Provider (Google / GitHub / Microsoft)
Browser->>tuvl: GET /auth/oauth/{provider}/start
tuvl->>tuvl: Generate CSRF state token (Redis or memory)
tuvl-->>Browser: 302 Redirect → Provider auth URL
Browser->>Provider (Google / GitHub / Microsoft): User logs in + consents
Provider (Google / GitHub / Microsoft)-->>Browser: 302 Redirect → /auth/oauth/{provider}/callback?code=...&state=...
Browser->>tuvl: GET /auth/oauth/{provider}/callback
tuvl->>tuvl: Validate CSRF state
tuvl->>Provider (Google / GitHub / Microsoft): POST token exchange
Provider (Google / GitHub / Microsoft)-->>tuvl: access_token + id_token
tuvl->>Provider (Google / GitHub / Microsoft): GET user profile (email, sub)
tuvl->>tuvl: Upsert IAMUser + auto-assign default_role (if configured)
tuvl-->>Browser: 302 Redirect → TUVL_OAUTH_UI_REDIRECT_URL?token=<biscuit_b64>
CSRF state tokens are stored in Redis (when configured) or in-process memory. In multi-worker deployments you must configure a Redis datasource to share state across workers.
Provider Configuration¶
File Layout¶
Each federation provider is a YAML file in <project>/federation/:
Google¶
kind: FederationProvider
version: v1
metadata:
name: google
description: Google OAuth2
enabled: true
spec:
provider: google
client_id: ${GOOGLE_CLIENT_ID}
client_secret: ${GOOGLE_CLIENT_SECRET}
scope: "openid email profile"
# Restrict login to users whose email domain is in this list.
# Omit to allow any address from the provider.
# allowed_domains:
# - example.com
# Role to assign automatically when a brand-new federated user logs in.
# The role must already exist in the IAM database.
# Omit to leave new users with no roles (admin must assign manually).
# default_role: member
Environment variables:
Built-in OAuth2 URLs (no override needed):
| Setting | Value |
|---|---|
| auth_url | https://accounts.google.com/o/oauth2/v2/auth |
| token_url | https://oauth2.googleapis.com/token |
| userinfo_url | https://www.googleapis.com/oauth2/v3/userinfo |
Google Cloud Console setup:
- Create a project at console.cloud.google.com
- Enable the People API or Google+ API
- Create an OAuth2 Client ID (Web Application)
- Add
{TUVL_OAUTH_BASE_URL}/auth/oauth/google/callbackto Authorized Redirect URIs
GitHub¶
kind: FederationProvider
version: v1
metadata:
name: github
spec:
provider: github
client_id: ${GITHUB_CLIENT_ID}
client_secret: ${GITHUB_CLIENT_SECRET}
scope: "read:user user:email"
Environment variables:
Private emails
If the user's GitHub email is set to private, tuvl automatically calls
GET https://api.github.com/user/emails to find their primary verified address.
GitHub App setup:
- Go to Settings → Developer settings → OAuth Apps → New
- Set Homepage URL to your base URL
- Set Authorization callback URL to
{TUVL_OAUTH_BASE_URL}/auth/oauth/github/callback
Microsoft (Entra ID)¶
kind: FederationProvider
version: v1
metadata:
name: microsoft
spec:
provider: microsoft
client_id: ${AZURE_CLIENT_ID}
client_secret: ${AZURE_CLIENT_SECRET}
scope: "openid email profile User.Read"
tenant_id: ${AZURE_TENANT_ID:common} # "common" allows any MS account
Environment variables:
AZURE_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
AZURE_CLIENT_SECRET=your-secret
AZURE_TENANT_ID=your-tenant-id # or "common" / "organizations" / "consumers"
Azure Portal setup:
- Register an app in Azure Active Directory → App registrations
- Under Authentication, add
{TUVL_OAUTH_BASE_URL}/auth/oauth/microsoft/callbackas a redirect URI - Add a client secret under Certificates & secrets
- Set the required
tenant_id(usecommonto allow any Microsoft account)
Custom Provider¶
For any OpenID Connect-compatible provider, supply all URLs explicitly:
kind: FederationProvider
version: v1
metadata:
name: okta
spec:
provider: custom
client_id: ${OKTA_CLIENT_ID}
client_secret: ${OKTA_CLIENT_SECRET}
scope: "openid email profile"
auth_url: https://{your-domain}.okta.com/oauth2/v1/authorize
token_url: https://{your-domain}.okta.com/oauth2/v1/token
userinfo_url: https://{your-domain}.okta.com/oauth2/v1/userinfo
Required Environment Variable¶
This tells tuvl what base URL to use when constructing the redirect URI sent to the provider.
In local development set it to http://localhost:8000.
After a successful OAuth login the browser is redirected to this URL with ?token=<biscuit_b64>
appended. Your frontend reads the token from the query string and passes it to TuvlClient.
When unset the callback returns JSON instead (useful for server-side and CLI flows).
Admin API¶
Federation provider files can be managed without filesystem access via the admin API.
Note
All federation admin endpoints require the iam:admin scope.
List Configured Providers¶
Returns an array of provider names: ["google", "github"]
Get Provider Config¶
Returns the raw YAML content. The client_secret field is always redacted ("***") in
the response regardless of whether it was set as a literal or an env-var reference.
Create / Update Provider¶
PUT /auth/admin/federation/{name}
Authorization: Bearer <admin_token>
Content-Type: application/json
Accepts the YAML object as JSON. The file is saved to <project>/federation/{name}.yaml
and the registry is reloaded immediately.
Delete Provider¶
Removes the YAML file and un-registers the provider immediately (no restart needed).
Account Linking¶
When a federated login succeeds, tuvl looks up a iam_users record by email:
- Found — the existing account is reused.
federated_providerandfederated_subare updated on the record if not already set. - Not found — a new user record is created with
is_active=true. The account has no password (federated-only).
The returned Biscuit token carries whatever roles the user has been assigned in the database.
First-time federated users have no roles by default
When a user logs in via OAuth for the first time they arrive with zero permissions.
Any workflow call that requires a scope will return 403 Forbidden.
To avoid this, add a default_role field to the provider YAML:
spec:
provider: google
client_id: ${GOOGLE_CLIENT_ID}
client_secret: ${GOOGLE_CLIENT_SECRET}
default_role: member # role must exist in the IAM database
tuvl will auto-assign that role the moment the user record is created. The
role assignment is permanent — it is not removed if you later remove default_role
from the YAML.
Domain Restriction¶
Use allowed_domains to limit login to specific email domains:
spec:
provider: google
client_id: ${GOOGLE_CLIENT_ID}
client_secret: ${GOOGLE_CLIENT_SECRET}
allowed_domains:
- acme.com
- acme.org
Users whose email domain is not in the list receive a 403 Forbidden at the callback step.
Leave the field absent (or empty) to allow any domain.