Skip to Content
Welcome to the new DocsGPT docs!
Deploying🔐 SSO with OIDC

SSO with OIDC

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.

How the flow works

  1. A user opens DocsGPT without a session. The frontend redirects the browser to GET /api/auth/oidc/login on the DocsGPT API.
  2. The backend starts an OAuth2 Authorization Code + PKCE flow and redirects to your IdP’s sign-in page.
  3. After sign-in, the IdP redirects back to 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.
  4. The browser returns to your frontend with a short-lived single-use code in the URL fragment; the frontend exchanges it for the session JWT and stores it. From here on, requests are authenticated exactly like the other AUTH_TYPE modes (Authorization: Bearer <token>).
  5. Signing out clears the local session and redirects through the IdP’s end-session endpoint.

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.

IdP compatibility notes

  • Token-endpoint authentication follows the IdP’s discovery document (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.
  • Userinfo fallback: when the ID token lacks the user-id claim (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.

Settings reference

SettingRequiredDefaultDescription
AUTH_TYPEyesSet to oidc.
OIDC_ISSUERyesIssuer URL of your IdP. Discovery is read from <issuer>/.well-known/openid-configuration.
OIDC_CLIENT_IDyesClient ID registered at the IdP.
OIDC_FRONTEND_URLyesBrowser-facing URL of the DocsGPT frontend (where users land after login/logout), e.g. https://docsgpt.example.com.
OIDC_CLIENT_SECRETnoSet when the IdP client is confidential. PKCE is always used, so public clients work without a secret.
OIDC_SCOPESnoopenid profile emailScopes requested at the IdP. Add offline_access when your IdP requires it for refresh tokens (Authentik does).
OIDC_USER_ID_CLAIMnosubID-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_URInoderivedFull 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_SECONDSno28800 (8h)Lifetime of the DocsGPT session JWT. Sessions renew before expiry — see Silent session renewal.
OIDC_PROVIDER_NAMEnoDisplay name on the sign-in button: Acme SSO renders “Sign in with Acme SSO”. Unset, the button shows a generic “SSO”.
OIDC_ALLOWED_GROUPSnoComma-separated group allowlist. Unset, any authenticated IdP user may sign in — see Restricting sign-in by group.
OIDC_GROUPS_CLAIMnogroupsID-token/userinfo claim carrying the user’s group membership.
JWT_SECRET_KEYrecommendedauto-generatedSigns 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.

Setting up with Authentik

  1. Create a provider. In the Authentik admin UI go to Applications → Providers → Create and pick OAuth2/OpenID Provider:
    • Authorization flow: your preferred flow (e.g. explicit consent).
    • Client type: Public (no secret, PKCE only) or Confidential (also set OIDC_CLIENT_SECRET in DocsGPT).
    • Redirect URIs: https://<your-docsgpt-api>/api/auth/oidc/callback
    • Signing key: select a certificate so ID tokens are RS256-signed.
  2. Create an application (Applications → Applications → Create), link it to the provider, and note its slug.
  3. Find the issuer. With Authentik’s default per-provider issuer mode it is:
    https://<your-authentik-host>/application/o/<application-slug>/
    (the trailing slash is part of the issuer — copy it exactly; the discovery document lives at .../<application-slug>/.well-known/openid-configuration).
  4. Configure DocsGPT in .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_access scope is requested — set OIDC_SCOPES=openid profile email offline_access.

Which claim becomes the user id?

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.

Keycloak (and other IdPs)

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.

Restricting sign-in by group

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:

  • Authentik includes group names in the groups claim through its default profile scope — no extra configuration needed.
  • Keycloak does not emit groups by default. On the client, open Client scopes → the client’s dedicated scope → Add mapper → By configuration → Group Membership, set the claim name to groups, and turn Full group path off so the claim carries plain names (devs) rather than paths (/devs).

Silent session renewal

The DocsGPT session JWT lives for OIDC_SESSION_LIFETIME_SECONDS (default 8 hours). Sessions renew without user-visible interruptions, in one of two ways:

  • With a refresh token. When the IdP issues one, the backend stores it server-side (in Redis — never in the browser) and the frontend calls 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.
  • Without a refresh token. The frontend lets the session run to expiry and then redirects through the IdP again. While the IdP session is still alive, this round-trip is also silent; the user only sees a sign-in page once the IdP session is gone too.

Getting a refresh token:

  • Keycloak issues refresh tokens for the authorization-code flow by default — nothing to change.
  • Authentik only issues refresh tokens when the 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.

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).

  • Keycloak: open the client → Settings and set Backchannel logout URL to https://<your-docsgpt-api>/api/auth/oidc/backchannel-logout.
  • Authentik (2025.8.0 and later; marked Preview): on the OAuth2/OpenID provider set Logout Method to Back-channel and Logout URI to the same URL — see the Authentik logout docs . Authentik sends the logout token when a user logs out, an admin deletes their session, the account is deactivated, or the session is revoked. On older Authentik versions back-channel logout is unavailable — revocation latency then falls back to the session lifetime, or use SCIM deactivation, which also revokes sessions instantly.

SCIM user provisioning

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.

SettingRequiredDefaultDescription
SCIM_ENABLEDyesfalseSet to true to serve the /scim/v2 endpoints.
SCIM_TOKENyesBearer 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>.

Match the SCIM userName to the OIDC user id

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.

What the endpoint supports

OperationSupport
GET /scim/v2/ServiceProviderConfig, /ResourceTypes, /SchemasDiscovery documents.
GET /scim/v2/UsersList, with the exact filter userName eq "..." and startIndex/count pagination (1-based, max 200 per page).
POST /scim/v2/UsersCreate; 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/GroupsGroup 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.

IdP setup pointers

  • Okta: add SCIM provisioning to the app integration with SCIM connector base URL = https://<your-docsgpt-api>/scim/v2 and authentication mode HTTP Header carrying the bearer token. Enable creating and deactivating users; skip group push.
  • Authentik: create a SCIM provider with the same base URL and the token, and attach it to the application as a backchannel provider. Sync users only — leave group mappings out, since DocsGPT answers group provisioning with 501.

Login auditing

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:

EventRecorded when
oidc_loginA user signs in successfully.
oidc_login_deniedA sign-in is rejected — metadata.reason is not_authorized (group allowlist) or account_disabled.
oidc_refreshA session is silently renewed.
backchannel_logoutThe IdP revokes sessions via back-channel logout.
scim_created / scim_deactivated / scim_reactivatedSCIM 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;

Troubleshooting

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:

CodeCause
invalid_stateThe login attempt expired (the state is held for 10 minutes) or was replayed. Retrying the sign-in usually fixes it.
auth_failedToken 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_claimNeither 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_authorizedThe user’s groups don’t intersect OIDC_ALLOWED_GROUPS — see Restricting sign-in by group.
account_disabledThe account was deactivated via SCIM or by an operator. Reactivate it over SCIM to restore access.

Other issues:

  • IdP shows a redirect URI error — the callback URL registered at the IdP must match exactly. Behind a reverse proxy, set OIDC_REDIRECT_URI to the public callback URL instead of relying on the derived default.
  • Revoked users can still access DocsGPT — without back-channel logout, sessions outlive IdP-side revocation until the next renewal or expiry. Configure back-channel logout for instant revocation, deactivate the user over SCIM, or lower OIDC_SESSION_LIFETIME_SECONDS.
  • SCIM requests fail404: SCIM_ENABLED is not true. 503: SCIM is enabled but SCIM_TOKEN is unset. 401: the presented bearer token doesn’t match SCIM_TOKEN.
  • Login endpoints return 503 — Redis is unreachable or the IdP discovery document can’t be fetched from the API host.