docs/content/Deploying/OIDC-SSO.mdx
Setting AUTH_TYPE=oidc makes DocsGPT delegate sign-in to an external OpenID Connect identity provider (IdP). Any spec-compliant IdP with a discovery document works; this guide uses Authentik as the reference provider and includes a short note for Keycloak.
Beyond basic sign-in, this page covers the optional access controls: group allowlists, silent session renewal, back-channel logout, SCIM user provisioning, and login auditing.
GET /api/auth/oidc/login on the DocsGPT API.GET /api/auth/oidc/callback. The backend exchanges the code server-side, validates the ID token (signature via JWKS, issuer, audience, expiry, nonce), and mints a DocsGPT session JWT signed with JWT_SECRET_KEY.AUTH_TYPE modes (Authorization: Bearer <token>).The user's identity (sub claim by default) becomes the DocsGPT user_id, so every user gets their own conversations, sources, agents, and settings.
Sessions last OIDC_SESSION_LIFETIME_SECONDS (8 hours by default) and renew without interrupting the user — see Silent session renewal.
Redis must be reachable by the API — it stores the short-lived login state, handoff codes, server-side refresh tokens, and the session revocation denylist. Redis is already a required DocsGPT dependency, so no extra infrastructure is needed.
token_endpoint_auth_methods_supported): client_secret_post when the IdP advertises it, otherwise HTTP Basic (the RFC default). Okta's default web-app configuration works without extra toggles.OIDC_USER_ID_CLAIM) — or the groups claim while a group allowlist is configured — the backend fetches the IdP's userinfo endpoint and merges the missing claims. ID-token values win on conflict, and the userinfo sub must match the ID token's.| Setting | Required | Default | Description |
|---|---|---|---|
AUTH_TYPE | yes | — | Set to oidc. |
OIDC_ISSUER | yes | — | Issuer URL of your IdP. Discovery is read from <issuer>/.well-known/openid-configuration. |
OIDC_CLIENT_ID | yes | — | Client ID registered at the IdP. |
OIDC_FRONTEND_URL | yes | — | Browser-facing URL of the DocsGPT frontend (where users land after login/logout), e.g. https://docsgpt.example.com. |
OIDC_CLIENT_SECRET | no | — | Set when the IdP client is confidential. PKCE is always used, so public clients work without a secret. |
OIDC_SCOPES | no | openid profile email | Scopes requested at the IdP. Add offline_access when your IdP requires it for refresh tokens (Authentik does). |
OIDC_USER_ID_CLAIM | no | sub | ID-token claim used as the DocsGPT user id. Set to email or preferred_username for human-readable ids; use email when provisioning over SCIM. |
OIDC_REDIRECT_URI | no | derived | Full callback URL registered at the IdP. Defaults to <request host>/api/auth/oidc/callback; set it explicitly when the API runs behind a reverse proxy. |
OIDC_SESSION_LIFETIME_SECONDS | no | 28800 (8h) | Lifetime of the DocsGPT session JWT. Sessions renew before expiry — see Silent session renewal. |
OIDC_PROVIDER_NAME | no | — | Display name on the sign-in button: Acme SSO renders "Sign in with Acme SSO". Unset, the button shows a generic "SSO". |
OIDC_ALLOWED_GROUPS | no | — | Comma-separated group allowlist. Unset, any authenticated IdP user may sign in — see Restricting sign-in by group. |
OIDC_GROUPS_CLAIM | no | groups | ID-token/userinfo claim carrying the user's group membership. |
JWT_SECRET_KEY | recommended | auto-generated | Signs DocsGPT session tokens. Set it explicitly in production — required when running multiple API replicas. |
SCIM_ENABLED and SCIM_TOKEN are listed in the SCIM section.
Public (no secret, PKCE only) or Confidential (also set OIDC_CLIENT_SECRET in DocsGPT).https://<your-docsgpt-api>/api/auth/oidc/callbackhttps://<your-authentik-host>/application/o/<application-slug>/
.../<application-slug>/.well-known/openid-configuration)..env and restart:
AUTH_TYPE=oidc
OIDC_ISSUER=https://auth.example.com/application/o/docsgpt/
OIDC_CLIENT_ID=<client id from step 1>
# OIDC_CLIENT_SECRET=<only for Confidential client type>
OIDC_FRONTEND_URL=https://docsgpt.example.com
JWT_SECRET_KEY=<long random string>
Planning to use silent session renewal? Authentik only issues refresh tokens when the
offline_accessscope is requested — setOIDC_SCOPES=openid profile email offline_access.
Authentik's provider setting Subject mode controls what lands in the sub claim (the default is a hashed user ID — stable but opaque). If you'd rather key DocsGPT users on something readable, either change Subject mode (e.g. based on username) or leave Authentik alone and set OIDC_USER_ID_CLAIM=email in DocsGPT. Pick one strategy before going live: changing it later gives existing users fresh, empty accounts. If you plan to provision users over SCIM, use OIDC_USER_ID_CLAIM=email — SCIM matches users by userName, which IdPs typically send as the email.
Any OIDC provider with discovery works the same way. For Keycloak:
OIDC_ISSUER=https://keycloak.example.com/realms/<realm>
OIDC_CLIENT_ID=<client id>
Create the client with Standard flow enabled and PKCE method S256; register the same /api/auth/oidc/callback redirect URI.
The feature sections below carry their own per-IdP notes — group claims, refresh tokens, back-channel logout, and SCIM each need one IdP-side setting.
By default any user who can authenticate at the IdP may use DocsGPT. To restrict access to specific IdP groups:
OIDC_ALLOWED_GROUPS=docsgpt-users,platform-admins
# OIDC_GROUPS_CLAIM=groups # only if your IdP uses a different claim name
At login the backend reads the OIDC_GROUPS_CLAIM claim (default groups) from the ID token, falling back to the userinfo endpoint when the claim is absent. A user whose groups share no entry with the allowlist is rejected with a clean "not authorized" screen (oidc_error=not_authorized), and the denial lands in the audit log.
Group changes take effect at the next sign-in or the next silent renewal: whenever the IdP returns a fresh ID token during renewal, the allowlist is re-checked — so removing a user from the allowed group cuts off their session at the next renewal instead of whenever they happen to sign in again.
Getting groups into the token:
groups claim through its default profile scope — no extra configuration needed.groups, and turn Full group path off so the claim carries plain names (devs) rather than paths (/devs).The DocsGPT session JWT lives for OIDC_SESSION_LIFETIME_SECONDS (default 8 hours). Sessions renew without user-visible interruptions, in one of two ways:
POST /api/auth/oidc/refresh about 15 minutes before the session expires. The backend redeems the refresh token at the IdP, re-validates the fresh ID token (including the group allowlist), mints a new session JWT, and rotates the stored refresh token. The user notices nothing.Getting a refresh token:
offline_access scope is requested:
OIDC_SCOPES=openid profile email offline_access
Revoking the user's consent or sessions at the IdP makes the next renewal fail, and the user must sign in again. For revocation that doesn't wait for the next renewal, configure back-channel logout.
DocsGPT implements OIDC Back-Channel Logout 1.0. The IdP POSTs a signed logout_token to:
POST https://<your-docsgpt-api>/api/auth/oidc/backchannel-logout
DocsGPT validates the token (signature via JWKS, issuer, audience, replay protection) and immediately revokes the user's live sessions through a Redis denylist — revoked requests get 401 with error: token_revoked. Signing the user out at the IdP, or an admin revoking their sessions there, takes effect on their next DocsGPT request instead of at session expiry.
The endpoint is called server-to-server, so it must be reachable from the IdP (it is not a browser redirect).
https://<your-docsgpt-api>/api/auth/oidc/backchannel-logout.DocsGPT exposes a SCIM 2.0 endpoint so your IdP can drive the user lifecycle: create accounts ahead of first login and — more importantly — deactivate them on offboarding. Deactivating a user revokes their live sessions immediately and blocks future sign-ins (they see an "account disabled" screen); reactivating restores access.
| Setting | Required | Default | Description |
|---|---|---|---|
SCIM_ENABLED | yes | false | Set to true to serve the /scim/v2 endpoints. |
SCIM_TOKEN | yes | — | Bearer token the IdP's SCIM client must present. Use a long random string. |
The base URL is https://<your-docsgpt-api>/scim/v2; every request must carry Authorization: Bearer <SCIM_TOKEN>.
SCIM identifies users by userName, which DocsGPT matches against its user id — the value of OIDC_USER_ID_CLAIM. With the default sub claim, the userName your IdP sends (typically the email) would never line up with the opaque sub of the same user signing in, and DocsGPT would treat them as two unrelated accounts. When using SCIM, set OIDC_USER_ID_CLAIM=email and have the IdP send the email as the SCIM userName.
| Operation | Support |
|---|---|
GET /scim/v2/ServiceProviderConfig, /ResourceTypes, /Schemas | Discovery documents. |
GET /scim/v2/Users | List, with the exact filter userName eq "..." and startIndex/count pagination (1-based, max 200 per page). |
POST /scim/v2/Users | Create; returns 409 when the userName already exists. |
GET /scim/v2/Users/<id> | Read. |
PUT / PATCH /scim/v2/Users/<id> | Activate/deactivate via the active attribute (Okta's string "true"/"false" values are accepted). userName is immutable; other attributes are ignored. |
DELETE /scim/v2/Users/<id> | Soft delete — deactivates the account instead of removing data. |
/scim/v2/Groups | Group provisioning is not supported: listing returns an empty result so IdP probes don't fail, and mutations return 501. Use the group allowlist for group-based access control instead. |
https://<your-docsgpt-api>/scim/v2 and authentication mode HTTP Header carrying the bearer token. Enable creating and deactivating users; skip group push.501.Authentication activity is recorded in auth_events, an append-only Postgres table carrying the user id, event name, IP address, user agent, a JSONB metadata column, and a timestamp:
| Event | Recorded when |
|---|---|
oidc_login | A user signs in successfully. |
oidc_login_denied | A sign-in is rejected — metadata.reason is not_authorized (group allowlist) or account_disabled. |
oidc_refresh | A session is silently renewed. |
backchannel_logout | The IdP revokes sessions via back-channel logout. |
scim_created / scim_deactivated / scim_reactivated | SCIM lifecycle changes. |
There is no UI for these events yet — query the table directly:
SELECT created_at, event, user_id, ip, metadata
FROM auth_events
ORDER BY created_at DESC
LIMIT 50;
When sign-in fails, the browser lands back on the frontend with an #oidc_error=<code> fragment and the sign-in screen shows a matching message:
| Code | Cause |
|---|---|
invalid_state | The login attempt expired (the state is held for 10 minutes) or was replayed. Retrying the sign-in usually fixes it. |
auth_failed | Token exchange or ID-token validation failed — check the API logs. Most common: OIDC_ISSUER doesn't match the issuer the discovery document reports (for Authentik this includes the application slug and trailing slash), or clock skew beyond the allowed 60 seconds. |
missing_claim | Neither the ID token nor userinfo contains OIDC_USER_ID_CLAIM. Make sure the matching scope is requested (OIDC_SCOPES) and the IdP actually emits the claim, or switch the setting back to sub. |
not_authorized | The user's groups don't intersect OIDC_ALLOWED_GROUPS — see Restricting sign-in by group. |
account_disabled | The account was deactivated via SCIM or by an operator. Reactivate it over SCIM to restore access. |
Other issues:
OIDC_REDIRECT_URI to the public callback URL instead of relying on the derived default.OIDC_SESSION_LIFETIME_SECONDS.404: SCIM_ENABLED is not true. 503: SCIM is enabled but SCIM_TOKEN is unset. 401: the presented bearer token doesn't match SCIM_TOKEN.