Configure OIDC / SSO login¶
Repod supports Single Sign-On via any standards-compliant OpenID Connect provider — Keycloak, Authentik, Zitadel, Azure AD (Entra ID), Okta, and ADFS have all been used with it. Once configured, users authenticate with your IdP and never enter a Repod-local password.
Repod implements the Authorization Code flow with PKCE (RFC 7636), the flow recommended for browser-based SPAs — there is no client secret exposed to the browser at any point; PKCE replaces it.
1. Prerequisites¶
- Repod
adminaccess to Settings → SSO. - An OIDC application registered in your IdP, with:
- A redirect URI pointing at
https://<your-repod-host>/oidc-callback - A client ID and client secret
- The
openid email profilescopes enabled (default) - Your IdP's discovery URL — the standard
/.well-known/openid-configurationdocument. Examples: - Keycloak:
https://sso.example.com/realms/myorg/.well-known/openid-configuration - Azure AD:
https://login.microsoftonline.com/<tenant-id>/v2.0/.well-known/openid-configuration - Okta:
https://<org>.okta.com/.well-known/openid-configuration
Enterprise feature
OIDC is gated behind license_svc.check_feature("oidc") — it requires an Enterprise license (on-premise) or a plan that includes SSO (SaaS). On Community/unlicensed installations, GET /api/v1/auth/oidc/public-config always returns {"enabled": false} regardless of what is saved in settings.
2. Step 1 — Register the redirect URI in your IdP¶
Before touching Repod, create the OIDC client application in your IdP and set its allowed redirect URI to:
This is a fixed frontend route (OidcCallbackPage.js) — it is not configurable per-provider beyond the host itself. If Repod is reachable at more than one hostname, register each one as a separate allowed redirect URI in the IdP.
3. Step 2 — Open SSO settings in Repod¶
- Log in to Repod as an administrator.
- Navigate to Settings → SSO.
- Toggle Enable SSO on. The rest of the form becomes editable.
4. Step 3 — Configure the connection¶
| Field | Settings key | Description |
|---|---|---|
| Provider name | provider_name |
Label shown on the "Sign in with…" button on the login page. Default SSO. |
| Discovery URL | discovery_url |
The IdP's /.well-known/openid-configuration URL. Repod fetches and caches this document for 5 minutes. |
| Client ID | client_id |
From your IdP's OIDC application registration. |
| Client secret | client_secret |
From the same registration. Stored encrypted at rest (SETTINGS_ENCRYPTION_KEY). |
| Scopes | scopes |
Space-separated OAuth scopes requested. Default openid email profile. |
| Redirect URI | redirect_uri |
Leave empty to auto-compute as <app_url>/oidc-callback. Only set explicitly if app_url doesn't match what you registered in the IdP. |
Click Test connection before saving — this calls POST /api/v1/auth/oidc/test-discovery with the discovery URL and reports back the resolved authorization_endpoint, token_endpoint, and jwks_uri, or a clear error if the discovery document couldn't be fetched.
curl -X POST https://repod.example.com/api/v1/auth/oidc/test-discovery \
-H "Authorization: Bearer $ADMIN_JWT" \
-H "Content-Type: application/json" \
-d '{"discovery_url": "https://sso.example.com/realms/myorg/.well-known/openid-configuration"}'
{
"ok": true,
"issuer": "https://sso.example.com/realms/myorg",
"auth_ep": "https://sso.example.com/realms/myorg/protocol/openid-connect/auth",
"token_ep": "https://sso.example.com/realms/myorg/protocol/openid-connect/token",
"jwks_uri": "https://sso.example.com/realms/myorg/protocol/openid-connect/certs"
}
5. Step 4 — Map claims and provisioning¶
| Field | Settings key | Default |
|---|---|---|
| Auto-provision accounts | auto_provision |
true — creates a local Repod user on first successful SSO login |
| Default role | default_role |
reader — role assigned when no group/role mapping matches |
| Username claim | claim_username |
preferred_username (falls back to sub if absent) |
| Email claim | claim_email |
email |
| Full name claim | claim_fullname |
name |
| Role claim | claim_role |
empty — the ID token claim carrying the user's IdP groups/roles (e.g. groups). Leave empty to always use default_role. |
| Role map | role_map |
{} — maps an IdP group/role value to a Repod role, e.g. {"repod-admins": "admin", "repod-uploaders": "uploader"} |
The role claim can be either a single string or a list (typical for a groups claim). Repod checks each candidate value against role_map and assigns the first match; if nothing matches, default_role applies.
A user provisioned via SSO gets auth_source="oidc" and a random, unusable local password — they can never log in with a Repod-local password, only via SSO.
If Auto-provision is off and an unknown user completes the SSO handshake, Repod rejects the login with 403 and a message telling them to contact an administrator — no account is silently created.
6. Step 5 — Save¶
Click Save. This sends a PATCH /api/v1/settings/ request with the oidc section:
curl -X PATCH https://repod.example.com/api/v1/settings/ \
-H "Authorization: Bearer $ADMIN_JWT" \
-H "Content-Type: application/json" \
-d '{
"oidc": {
"enabled": true,
"provider_name": "Corporate SSO",
"discovery_url": "https://sso.example.com/realms/myorg/.well-known/openid-configuration",
"client_id": "repod",
"client_secret": "s3cr3t",
"scopes": "openid email profile",
"auto_provision": true,
"default_role": "reader",
"claim_username": "preferred_username",
"claim_role": "groups",
"role_map": {"repod-admins": "admin", "repod-uploaders": "uploader"}
}
}'
GET /api/v1/settings/ returns the current configuration with client_secret masked (••••••••) — re-saving without changing the secret is safe, since the masked placeholder is stripped from the update rather than overwriting the real stored value.
7. How the login flow works¶
- The Repod login page calls
GET /api/v1/auth/oidc/public-config(no auth required). If SSO is enabled and licensed, it shows a "Sign in with<provider_name>" button. - The frontend generates a PKCE
code_verifier/code_challengepair and a randomstate, then callsPOST /api/v1/auth/oidc/authorizewith the challenge and state. Repod returns the IdP's authorization URL. - The browser is redirected to the IdP, the user authenticates there, and the IdP redirects back to
/oidc-callbackwith acode. - The frontend calls
POST /api/v1/auth/oidc/callbackwithcode,state, andcode_verifier. Repod exchanges the code for an ID token against the IdP's token endpoint, validates its signature against the IdP's JWKS, extracts the claims, resolves (or provisions) the local user, and returns a normal Repodaccess_token.
From this point on, the SSO-issued JWT is identical to one issued by local or LDAP login — every API call uses the same Authorization: Bearer <token> header.
8. Verify it worked¶
- Open the Repod login page in an incognito/private window. Confirm the "Sign in with
<provider_name>" button appears. - Click it, authenticate against your IdP, and confirm you land back on the Repod dashboard.
- Check Settings → Users (or
GET /api/v1/auth/users, admin only) for the newly provisioned account —auth_sourceshould readoidc. - Check the audit log for an
OIDC_PROVISIONentry (first login only) followed byOIDC_LOGIN: - If the button doesn't appear, re-check
enabled: truewas actually saved (GET /api/v1/settings/) and that your license includes theoidcfeature.
9. Troubleshooting¶
Login button never appears
GET /api/v1/auth/oidc/public-configreturns{"enabled": false}ifoidc.enabledis false in settings or the license doesn't include theoidcfeature — the response doesn't distinguish the two, by design (avoids leaking licensing details on an unauthenticated endpoint).
401 on callback: "ID token invalide ou expiré"
- The IdP's clock is out of sync with the Repod server, or the
client_idused to validate the token'saudclaim doesn't match what the IdP issued it for. Re-check the client ID field.
403 after successful IdP login: "Utilisateur inconnu"
auto_provisionis off and no local account exists for that username. Either create the account manually first, or enable auto-provisioning.
Discovery test fails with a network error
- Confirm the backend container can reach the discovery URL:
docker compose exec backend-api curl -sf <discovery_url>. A corporate IdP behind a VPN/firewall may not be reachable from the Repod host.