| title | SSO Quick Start (OIDC) |
|---|
Configure OpenRag to delegate authentication to your corporate SSO (LemonLDAP::NG, Keycloak, Auth0, Azure AD, Okta…) in five steps.
New to OIDC? You just need to coordinate with your SSO admin and set six
.envvariables. No code to change.
Give them the following information.
⚠
<openrag-host>is the OpenRag backend URL, NOT the indexer-ui front. The OIDC callback and back-channel-logout endpoints are hosted only on the backend (the container runningopenrag/api.py). If you point the IdP at the front URL instead, you get an infinite redirect loop: the front has no/auth/callbackroute, returns 404/401, which is interpreted as "not authenticated", triggering a new OIDC flow, which again lands on the front, etc.In a typical deployment:
https://rag.mycorp.com(the backend) vshttps://ui.mycorp.com(the front). Use the backend URL for the OIDC endpoints below.
| Field | Value to give |
|---|---|
| Client type | confidential (server-to-server token exchange) |
| Grant type | authorization_code |
| Response type | code |
| Valid redirect URIs | <openrag-backend-host>/auth/callback (backend, not front!) |
| Back-channel logout URI | <openrag-backend-host>/auth/backchannel-logout (backend!) |
| Post-logout redirect URIs | an optional URL outside OpenRag (see §Step 4 below) |
| Allowed scopes | openid, email, profile, offline_access |
Include sid in tokens |
✅ enabled (required for back-channel logout) |
| Send refresh token | ✅ enabled (so the session doesn't drop every few minutes) |
Then ask the admin for three pieces of information:
client_id— a public identifier, typicallyopenragor similar.client_secret— a long random string, shown only once by most IdPs. Store it in a password manager.- The IdP issuer URL — e.g.
https://sso.mycorp.com/orhttps://keycloak.mycorp.com/realms/mycorp.
This is the most common setup mistake. The issuer value you put in .env MUST match byte-for-byte what the IdP's discovery document advertises (including trailing slash, per OIDC Core §2). Keycloak usually has no trailing slash; LemonLDAP::NG and Auth0 usually have one.
Run this command against your IdP:
curl -s https://sso.mycorp.com/.well-known/openid-configuration | jq -r .issuerCopy the output verbatim. If you get:
https://sso.mycorp.com/→ use that with the slash.https://keycloak.mycorp.com/realms/mycorp→ use that without a slash.
Any mismatch and OpenRag refuses the login with Issuer mismatch in the logs.
Access tokens and refresh tokens returned by the IdP are stored encrypted at rest. Generate a dedicated key for your deployment:
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"Output example: XFlT-ZfXkdqf0v-5Z8kVt9xhU6c7Z4z0ZY8Z4Z4Z4= (44 chars, url-safe base64).
Store this in your secrets manager — losing it invalidates every stored session.
Copy this block at the end of your .env and fill in your values:
# Switch OpenRag from the legacy Bearer-token login to OIDC. REQUIRED.
AUTH_MODE=oidc
# Issuer URL — EXACT match with the curl|jq output from Step 2.
OIDC_ENDPOINT=https://sso.mycorp.com/
OIDC_CLIENT_ID=openrag
OIDC_CLIENT_SECRET=change-me-the-secret-from-step-1
# Must match EXACTLY the "Valid redirect URI" registered in Step 1.
OIDC_REDIRECT_URI=https://rag.mycorp.com/auth/callback
# From Step 3.
OIDC_TOKEN_ENCRYPTION_KEY=XFlT-ZfXkdqf0v-5Z8kVt9xhU6c7Z4z0ZY8Z4Z4Z4=
# --- Optional ---
# OIDC_SCOPES="openid email profile offline_access" # default
# OIDC_CLAIM_SOURCE=id_token # default ; alternative: userinfo
# OIDC_CLAIM_MAPPING= # default: empty (no sync of display_name/email from IdP)
# OIDC_AUTO_PROVISION_LOGIN=false # default ; true = create users on first login from claims (see Step 5)
# ⚠ Where the IdP sends the user AFTER logging out.
# A ("/") lands on the OpenRag root, which immediately re-triggers
# OIDC login — if the IdP session is still alive you appear to be
# re-logged-in instantly (no apparent "logout" effect); if it was killed
# you land back on the IdP form in a loop. Prefer a URL OUTSIDE OpenRag
# or nothing to let SSO doing its job:
# - your corporate intranet / landing page
# - a static "you are logged out" page you control
# - the IdP's own post-logout URL (e.g. https://sso.mycorp.com/)
# OIDC_POST_LOGOUT_REDIRECT_URI=https://intranet.mycorp.com/Tip — values with spaces (like
OIDC_SCOPES): quote them to stay safe across dotenv parsers:OIDC_SCOPES="openid email profile offline_access". Quotes are stripped on read.
By default, OpenRag never modifies a user's display_name or email after login. If you want the IdP to be the source of truth (useful when HR changes a user's name), set:
OIDC_CLAIM_MAPPING=display_name:name,email:emailEach pair is db_field:oidc_claim. Only display_name and email are writable — is_admin, external_user_id, file_quota, and token can never be changed via the IdP.
By default OpenRag reads the claims from the verified ID token (OIDC_CLAIM_SOURCE=id_token, no extra HTTP call). Switch to userinfo if your IdP only exposes certain claims via the /userinfo endpoint.
By default, OpenRag does not auto-create users on first login. Each user must exist in the database with their OIDC sub stored in external_user_id.
Skip this step entirely by setting
OIDC_AUTO_PROVISION_LOGIN=truein your.env. The callback then creates a non-admin user from the ID-token claims on first login and keepsdisplay_name+
Ask the IdP admin for each user's sub claim value (stable identifier, NOT the username). Then create the user via the OpenRag admin API — you'll need an admin AUTH_TOKEN for this:
# Boot once with AUTH_MODE=token and AUTH_TOKEN=sk-... to create users,
# OR keep AUTH_TOKEN in .env alongside AUTH_MODE=oidc — in that mode the
# bearer is still accepted for programmatic admin calls.
curl -X POST https://rag.mycorp.com/users/ \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"display_name": "Alice Cooper",
"external_user_id": "alice@mycorp.com",
"email": "alice@mycorp.com",
"is_admin": false
}'external_user_idmust equal the user's OIDCsub. If you don't know it, ask the admin to check with a test login (thesubis the.subclaim in the ID token).emailis optional metadata; not used for matching.is_admin: truegrants full admin rights inside OpenRag.
If a user tries to log in and their sub isn't pre-provisioned, OpenRag returns 403 User not registered and logs the sub so you can complete provisioning.
docker compose up --build -d
# Watch the startup logs for "OIDC authentication mode enabled"
docker compose logs openrag --tail 50 | grep -i OIDCOpen your browser at https://rag.mycorp.com/ → it redirects to your SSO → you log in → you come back authenticated.
If something goes wrong, see the full troubleshooting section in the OIDC guide. Most issues fall into one of three categories:
- Issuer mismatch (Step 2 — trailing slash).
- Invalid redirect URI (Step 1 — must match byte-for-byte).
- User not registered (Step 5 —
external_user_id≠sub).
Once AUTH_MODE=oidc, human users go through SSO. CI pipelines, scripts, and external agents keep working by using the per-user bearer token (users.token) — the same one returned at POST /users/ creation. Example:
# The token printed when you created alice in Step 5:
curl -H "Authorization: Bearer or-xxxxxxxxxxxxxxxx" https://rag.mycorp.com/v1/modelsThis gives you the best of both worlds: human-friendly SSO for the UI, token-based auth for automation.