Skip to Content
Deploy & Operate๐Ÿ” 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_TYPEyesโ€”Set to oidc.
OIDC_ISSUERyesโ€”Issuer URL of your IdP. Discovery is read from <issuer>/.well-known/openid-configuration.
OIDC_CLIENT_IDyesโ€”Client ID registered at the IdP.
OIDC_FRONTEND_URLyesโ€”Browser-facing URL of the DocsGPT frontend (where users land after login/logout), e.g. https://docsgpt.example.com.
OIDC_CLIENT_SECRETnoโ€”Set 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_NAMEnoโ€”Display name on the sign-in button: Acme SSO renders โ€œSign in with Acme SSOโ€. Unset, the button shows a generic โ€œSSOโ€.
OIDC_ALLOWED_GROUPSnoโ€”Comma-separated group allowlist. Unset, any authenticated IdP user may sign in โ€” see Restricting sign-in by group.
OIDC_ADMIN_GROUPSnoโ€”Comma-separated groups whose members are granted the global admin role. Re-checked at every login and renewal โ€” see Granting admin via groups.
OIDC_GROUPS_CLAIMnogroupsID-token/userinfo claim carrying the userโ€™s group membership.
JWT_SECRET_KEYrequired in productionauto-generated for local developmentSigns DocsGPT session tokens and agent-avatar capabilities. Every API and worker replica must use the same value.

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

Granting admin via groups

Separately from who may sign in, you can map an IdP group to the global admin role with OIDC_ADMIN_GROUPS:

OIDC_ADMIN_GROUPS=platform-admins

Members of the listed groups are granted admin; the mapping is re-evaluated at every login and every silent renewal, so removing a user from the admin group revokes their admin at the next renewal (exactly like the sign-in allowlist). It is independent of OIDC_ALLOWED_GROUPS, and leaving it unset never mass-revokes admin. On a fresh deployment, bootstrap the first admin with this mapping or with the docsgpt grant-admin command (see Bootstrapping the first admin). For the full roles, teams, and admin-dashboard model see Access Control, Roles & Teams.

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_TOKENyesโ€”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>.

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 event name, IP address, user agent, a JSONB metadata column, a timestamp, and two attribution columns:

  • actor_id โ€” who performed the action. Never empty.
  • target_id โ€” the user the action was performed on, or NULL when the event is not about a user (a team change, an instance-wide policy).

They differ whenever someone acts on someone else: an admin deactivating an account is actor_id = <the admin>, target_id = <the account>. For self-service events such as a login they are the same user. Automated actors are named system:oidc (a group-driven role change at login) and system:scim (provisioning).

A BEFORE INSERT trigger fills both columns for any row that arrives without an actor_id, applying the same rules as the migrationโ€™s backfill. That keeps attribution intact for a writer from a release that predates the columns โ€” during a rolling deployment, for instance โ€” instead of rejecting the insert or flattening the actor to a sentinel.

The events this page covers are oidc_login, oidc_login_denied (metadata.reason is not_authorized or account_disabled), oidc_refresh, backchannel_logout, scim_created / scim_deactivated / scim_reactivated, and role_granted / role_revoked with metadata.source = oidc_group for a group-driven change. The same table also records admin, team, quota, token and data-plane events; the full list is the audit event table.

See Access Control, Roles & Teams for the admin Activity view that reads these events, and for the two other journals it merges them with. To query the table directly:

SELECT created_at, event, actor_id, target_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 admin. Reactivate it over SCIM, or from Admin โ†’ Users (PATCH /api/admin/users/<id> with {"active": true}).

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.
  • The API refuses to start โ€” settings validation fails before the app loads. AUTH_TYPE=oidc requires settings: OIDC_ISSUER, ... names the missing OIDC_ISSUER, OIDC_CLIENT_ID or OIDC_FRONTEND_URL. SCIM_ENABLED requires settings: SCIM_TOKEN means SCIM is on without a token.
  • SCIM requests fail โ€” 404: SCIM_ENABLED is not true. 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.