SSO Setup
SSO Setup
Section titled “SSO Setup”Configure Single Sign-On (SSO) authentication flow in Mantis.
Overview
Section titled “Overview”Mantis implements OIDC Authorization Code flow with PKCE for secure SSO authentication.
Prerequisites
Section titled “Prerequisites”Before configuring SSO:
- Identity provider configured - See OIDC Configuration
- Callback URL configured in IdP
- Mandible external URL configured
Server Configuration
Section titled “Server Configuration”Ensure external_url is configured in Mandible:
[server]external_url = "https://api.mantis.local"Or via environment variable:
MANDIBLE__SERVER__EXTERNAL_URL=https://api.mantis.localRedirect URI to register with the provider
Section titled “Redirect URI to register with the provider”Mandible completes the authorization-code exchange itself, so the redirect URI you
register with the identity provider is Mandible’s callback — derived from
external_url:
<external_url>/api/v1/auth/oidc/callbackIt must match exactly, including scheme and any port. The web UI does not receive the authorization code and must not be registered here.
Authentication Flow
Section titled “Authentication Flow”Step 1: Initiate Login
Section titled “Step 1: Initiate Login”The UI requests an authorization URL:
curl -X POST \ -H "Content-Type: application/json" \ -d '{"redirect_url": "https://lens.mantis.local/dashboard"}' \ "https://api.mantis.local/api/v1/auth/oidc/$IDP_SQID/login"Response:
{ "authorization_url": "https://company.okta.com/oauth2/v1/authorize?response_type=code&client_id=xxx&redirect_uri=https://api.mantis.local/api/v1/auth/oidc/callback&scope=openid%20profile%20email&state=abc123&nonce=def456&code_challenge=xyz789&code_challenge_method=S256", "state": "abc123"}Step 2: User Authentication
Section titled “Step 2: User Authentication”The user is redirected to the IdP login page:
- User enters credentials
- IdP authenticates user
- IdP redirects to callback URL with authorization code
Step 3: Token Exchange
Section titled “Step 3: Token Exchange”The callback endpoint processes the response:
# IdP redirects to:GET /api/v1/auth/oidc/callback?code=xxx&state=abc123The API:
- Validates state parameter (CSRF protection)
- Exchanges code for tokens using PKCE verifier
- Validates ID token (signature, claims)
- Creates or updates user
- Returns Mantis JWT tokens
Security Features
Section titled “Security Features”PKCE (Proof Key for Code Exchange)
Section titled “PKCE (Proof Key for Code Exchange)”Prevents authorization code interception:
| Parameter | Value | Purpose |
|---|---|---|
code_challenge_method | S256 | SHA-256 hashing |
code_challenge | Base64URL(SHA256(verifier)) | Sent with authorization request |
code_verifier | Random 43-128 chars | Sent with token request |
State Parameter
Section titled “State Parameter”Prevents CSRF attacks:
Prevents replay attacks:
- Generated for each login attempt
- Included in ID token
- Verified during token validation
- Constant-time comparison prevents timing attacks
Auth State Management
Section titled “Auth State Management”-- Auth state stored in databaseCREATE TABLE oidc_auth_states ( id UUID PRIMARY KEY, state TEXT NOT NULL UNIQUE, nonce TEXT NOT NULL, code_verifier TEXT NOT NULL, provider_id UUID NOT NULL, redirect_url TEXT, created_at TIMESTAMP NOT NULL, expires_at TIMESTAMP NOT NULL, used_at TIMESTAMP);Auth states:
- Expire after 10 minutes (fixed — not configurable; there is no config key or env var for the auth-state TTL)
- Single-use (consumed on callback)
- Automatically cleaned up
User Provisioning
Section titled “User Provisioning”Auto-Create Users
Section titled “Auto-Create Users”Two things are required before a first-time SSO login can provision an account:
auto_create_usersis enabled on the provider. It is off unless you set it, so the first login against a new provider is refused until you do.- The provider is bound to a tenant. A system-wide provider (no
tenant_id) may link an existing user, but it will not create a brand-new one — a tenant-less account carries global scope, so minting one on every external login would hand out cross-tenant visibility. Settenant_idon the provider to enable provisioning.
With both in place:
User Creation Process
Section titled “User Creation Process”-
Extract claims from ID token:
sub- External user IDpreferred_usernameor configured claim - Usernameemailor configured claim - Email address
-
Generate unique username if needed:
alice -> alicealice (exists) -> alice2alice2 (exists) -> alice3 -
Create user record with:
- Random password (unused for SSO)
email_verified = false— provisioned SSO users are always created unverified; the IdP’semail_verifiedclaim is not propagated to the record- Tenant from provider (if configured)
-
Assign default role (if configured)
-
Create external identity link
External Identity Link
Section titled “External Identity Link”Links Mantis users to IdP identities:
CREATE TABLE external_identities ( id UUID PRIMARY KEY, user_id UUID NOT NULL, provider_id UUID NOT NULL, external_user_id TEXT NOT NULL, external_username TEXT, external_email TEXT, access_token_encrypted TEXT, refresh_token_encrypted TEXT, token_expires_at TIMESTAMP, last_login_at TIMESTAMP, created_at TIMESTAMP NOT NULL, UNIQUE (provider_id, external_user_id));ID Token Validation
Section titled “ID Token Validation”OIDC Core Validation (Section 3.1.3.7)
Section titled “OIDC Core Validation (Section 3.1.3.7)”| Check | Description |
|---|---|
| Signature | Verify using JWKS keys |
iss | Must match issuer URL |
aud | Must contain client_id |
azp | If multiple audiences, must equal client_id |
exp | Token not expired |
iat | Token not too old (5 min max) |
nonce | Must match expected nonce |
at_hash | If present, validates access token binding |
JWKS Caching
Section titled “JWKS Caching”For performance, JWKS keys are cached:
| Setting | Value |
|---|---|
| Cache TTL | 1 hour |
| Refresh interval | 1 minute minimum |
| Refresh trigger | Unknown key ID |
Session Management
Section titled “Session Management”JWT Tokens
Section titled “JWT Tokens”After successful SSO:
{ "access_token": "eyJhbGciOiJFUzI1NiIs...", "token_type": "Bearer", "expires_in": 3600, "user": { "id": "user_abc123", "email": "alice@company.com", "username": "alice", "roles": ["operator"] }}Refresh Token Cookie
Section titled “Refresh Token Cookie”Refresh token is set as HTTP-only cookie:
Set-Cookie: mantis_refresh_token=xxx; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=2592000Redirect URL Handling
Section titled “Redirect URL Handling”Post-Login Redirect
Section titled “Post-Login Redirect”redirect_url on the login request is validated against the allowlist and stored
on the auth state. After the callback completes the exchange, Mandible redirects the
browser to that URL, so it is where the user lands once logged in. If it is omitted,
the browser is sent to the application root.
Because the redirect is what returns the user to the app, the URL must pass the
allowlist. In release builds neither allow_any_redirect nor allow_localhost_redirect
defaults on, so allowed_redirect_urls (or allowed_redirect_domains) has to include
the application origin or login fails at initiation.
curl -X POST \ -H "Content-Type: application/json" \ -d '{"redirect_url": "https://lens.mantis.local/deployments"}' \ "https://api.mantis.local/api/v1/auth/oidc/$IDP_ID/login"Redirect URL Validation
Section titled “Redirect URL Validation”Redirect URLs are validated against an allowlist:
[oidc]# Entries are matched by scheme + host + port and a path PREFIX. A literal "*"# is just a path segment and matches nothing useful; a root URL already permits# every path on that origin, so the second line below is redundant, not a wildcard.allowed_redirect_urls = [ "https://lens.mantis.local"]
# For wildcard hosts, use allowed_redirect_domains (matched by domain suffix):allowed_redirect_domains = [ "*.mantis.local"]Listing Enabled Providers
Section titled “Listing Enabled Providers”The login page retrieves available SSO providers:
curl "https://api.mantis.local/api/v1/auth/oidc/providers"Response:
[ { "id": "idp_abc123", "name": "Okta", "provider_type": "oidc" }, { "id": "idp_def456", "name": "Azure AD", "provider_type": "oidc" }]Error Handling
Section titled “Error Handling”Common Errors
Section titled “Common Errors”| Error | Cause | Solution |
|---|---|---|
Invalid state | CSRF attempt or expired | Restart login flow |
Nonce mismatch | Replay attack or bug | Restart login flow |
Token expired | ID token too old | Check server clock sync |
Invalid audience | Wrong client_id | Verify IdP configuration |
Provider disabled | IdP not enabled | Enable provider |
IdP Error Response
Section titled “IdP Error Response”IdP may return errors in callback:
GET /callback?error=access_denied&error_description=User%20denied%20accessThese are returned to the client:
{ "error": "IdP returned error: access_denied - User denied access"}Rate Limiting
Section titled “Rate Limiting”The auth routes share a single rate limiter keyed by client IP. It counts failed authentication attempts only (successful calls are not counted): after 10 failed attempts per minute the IP is locked out for 900 seconds. The limit is shared across the auth routes, not a separate per-endpoint budget.
| Scope | Trigger | Effect |
|---|---|---|
| Auth routes (per client IP) | 10 failed attempts/min | 900s lockout |
Configuration:
[rate_limit.auth]enabled = truelogin_attempts_per_minute = 10lockout_duration_secs = 900Audit Logging
Section titled “Audit Logging”SSO events are logged:
{ "event": "auth.sso_login", "category": "authentication", "severity": "security", "actor": { "user_id": 42, "username": "alice" }, "metadata": { "provider_id": 1, "provider_name": "Okta", "external_user_id": "00u1234567890", "created": false }, "outcome": "success"}Troubleshooting
Section titled “Troubleshooting”Login Redirects to Error Page
Section titled “Login Redirects to Error Page”- Check callback URL matches exactly in IdP
- Verify external_url is configured
- Check state parameter validity window
User Not Created
Section titled “User Not Created”- Verify auto_create_users is enabled and the provider is bound to a tenant
- Check email claim is present in ID token
- Verify default role is assigned
Groups Not Synced
Section titled “Groups Not Synced”- Verify groups scope is requested
- Check groups_claim name matches IdP
- Verify role mappings are configured
Token Validation Fails
Section titled “Token Validation Fails”- Check server clock is synchronized
- Verify issuer URL matches exactly
- Check JWKS endpoint is accessible
Next Steps
Section titled “Next Steps”- Role Mappings - Map IdP groups to roles
- External Identities - Manage linked accounts
- OIDC Configuration - Provider setup
