Skip to content

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 admin access 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 profile scopes enabled (default)
  • Your IdP's discovery URL — the standard /.well-known/openid-configuration document. 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:

https://<your-repod-host>/oidc-callback

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

  1. Log in to Repod as an administrator.
  2. Navigate to Settings → SSO.
  3. 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

  1. 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.
  2. The frontend generates a PKCE code_verifier/code_challenge pair and a random state, then calls POST /api/v1/auth/oidc/authorize with the challenge and state. Repod returns the IdP's authorization URL.
  3. The browser is redirected to the IdP, the user authenticates there, and the IdP redirects back to /oidc-callback with a code.
  4. The frontend calls POST /api/v1/auth/oidc/callback with code, state, and code_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 Repod access_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

  1. Open the Repod login page in an incognito/private window. Confirm the "Sign in with <provider_name>" button appears.
  2. Click it, authenticate against your IdP, and confirm you land back on the Repod dashboard.
  3. Check Settings → Users (or GET /api/v1/auth/users, admin only) for the newly provisioned account — auth_source should read oidc.
  4. Check the audit log for an OIDC_PROVISION entry (first login only) followed by OIDC_LOGIN:
    curl -H "Authorization: Bearer $ADMIN_JWT" \
      "https://repod.example.com/api/v1/audit/logs?action=OIDC_LOGIN&per_page=5"
    
  5. If the button doesn't appear, re-check enabled: true was actually saved (GET /api/v1/settings/) and that your license includes the oidc feature.

9. Troubleshooting

Login button never appears

  • GET /api/v1/auth/oidc/public-config returns {"enabled": false} if oidc.enabled is false in settings or the license doesn't include the oidc feature — 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_id used to validate the token's aud claim doesn't match what the IdP issued it for. Re-check the client ID field.

403 after successful IdP login: "Utilisateur inconnu"

  • auto_provision is 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.