OIDC / SSO Integration Guide
Pulp Engine supports SSO authentication via OpenID Connect (OIDC). When enabled, users can sign in to the editor through your identity provider (Okta, Azure AD/Entra ID, Auth0, Keycloak, Google Workspace, etc.) instead of using personal API keys.
OIDC sits alongside the existing authentication methods — API keys and named-user keys continue to work. You can run a deployment with OIDC only, OIDC plus API keys, or no OIDC at all.
Quick start
-
Register Pulp Engine as a confidential OAuth client with your identity provider.
-
Configure the redirect URI as
https://your-api.example.com/auth/oidc/callback. -
Set the following environment variables on the API:
OIDC_DISCOVERY_URL=https://your-idp.example.com OIDC_CLIENT_ID=your-client-id OIDC_CLIENT_SECRET=your-client-secret OIDC_REDIRECT_URI=https://your-api.example.com/auth/oidc/callback OIDC_COOKIE_SECRET=$(openssl rand -hex 32) EDITOR_USERS_FILE=/var/pulp-engine/users.json -
Restart the API. The editor login screen will show a “Sign in with SSO” button.
Configuration reference
Required when OIDC is enabled
| Variable | Description |
|---|---|
OIDC_DISCOVERY_URL | Issuer URL — the server fetches {url}/.well-known/openid-configuration at startup. |
OIDC_CLIENT_ID | Confidential client ID registered with the OIDC provider. |
OIDC_CLIENT_SECRET | Confidential client secret. |
OIDC_REDIRECT_URI | Absolute callback URL. Must match what is registered with the provider. Must use HTTPS in hardened production mode. |
OIDC_COOKIE_SECRET | 32+ character secret used to sign the short-lived PKCE state cookie. Generate with openssl rand -hex 32. |
Optional
| Variable | Default | Description |
|---|---|---|
OIDC_SCOPES | openid profile email | Space-separated OAuth scopes to request. |
OIDC_CLAIM_SUB | sub | Claim used as the OIDC subject identifier. |
OIDC_CLAIM_EMAIL | email | Claim used to populate user email. |
OIDC_CLAIM_DISPLAY_NAME | name | Claim used as the user’s display name. |
OIDC_CLAIM_GROUPS | groups | Claim used to read group/role membership. |
OIDC_ADMIN_GROUPS | (none) | Comma-separated group values mapped to admin scope. |
OIDC_EDITOR_GROUPS | * | Comma-separated group values mapped to editor scope. * grants editor access to any authenticated user. Empty string means no default access — only users in OIDC_ADMIN_GROUPS can sign in. |
OIDC_AUTO_PROVISION | true | When true, new users are auto-created in the user registry on first OIDC login. Requires EDITOR_USERS_FILE for persistence across restarts. |
OIDC_PROVIDER_NAME | SSO | Display name for the SSO button in the editor. |
Authorization model
OIDC users are mapped to Pulp Engine scopes through group claims:
- The server reads the configured
OIDC_CLAIM_GROUPSclaim from the verified ID token. - If any value matches
OIDC_ADMIN_GROUPS, the user receivesadminscope. - Otherwise, if any value matches
OIDC_EDITOR_GROUPS(orOIDC_EDITOR_GROUPS=*), the user receiveseditorscope. - Otherwise, the user is denied access (403).
The resulting scope is identical to the scopes used by API key authentication, so all existing route authorization, audit attribution, and rate limiting apply unchanged.
Auto-provisioning and the user registry
When OIDC_AUTO_PROVISION=true (the default), the first time a user signs in via OIDC their record is created in the Pulp Engine user registry. The record includes:
id: derived from the OIDCsubclaim (sanitized to[a-zA-Z0-9_-], truncated to 64 chars)displayName: from the configured display name claimoidcSub: the originalsubclaimauthMethod: 'oidc'(distinguishes OIDC users from key-based users)key: a random UUID (never used for authentication — OIDC users authenticate via their IDP)
Persistence: auto-provisioned users only survive a restart when EDITOR_USERS_FILE is set. Without it, OIDC users live in memory and must re-provision on every restart. This is acceptable for stateless deployments but typically you want a persistent file. The startup logs warn when this is misconfigured.
Pre-provisioned mode: set OIDC_AUTO_PROVISION=false to require operators to create OIDC users manually via POST /admin/users. The user record must include authMethod: "oidc" and oidcSub: "<provider-sub>".
De-provisioning: removing an OIDC user requires DELETE /admin/users/:id. The replaceAll operation (used by bulk user updates) preserves OIDC-provisioned users automatically — they cannot be removed by omitting them from a bulk update.
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET | /auth/oidc/login | Redirects the browser to the OIDC authorization endpoint with PKCE + nonce. |
GET | /auth/oidc/callback | Handles the provider redirect, exchanges the code for tokens, mints a Pulp Engine session, redirects with a one-time completion code. |
POST | /auth/oidc/complete | Exchanges a single-use completion code for a Pulp Engine editor token. |
POST | /auth/oidc/exchange | Embed mode: exchanges an OIDC id_token directly for a Pulp Engine editor token. |
GET | /auth/oidc/logout | RP-initiated logout — redirects to the provider’s end_session_endpoint. |
Provider-specific setup
Okta
-
Create a new OIDC application of type Web.
-
Set the sign-in redirect URI to
https://your-api.example.com/auth/oidc/callback. -
Under Sign On → OpenID Connect ID Token, ensure
groupsclaim is included. Add a claim filter (e.g.Matches regex: .*) to send all groups, or scope it. -
Note the issuer (typically
https://your-org.okta.com), client ID, and client secret. -
Configure Pulp Engine:
OIDC_DISCOVERY_URL=https://your-org.okta.com OIDC_CLIENT_ID=... OIDC_CLIENT_SECRET=... OIDC_ADMIN_GROUPS=pulp-engine-admins OIDC_EDITOR_GROUPS=pulp-engine-editors
Azure AD / Entra ID
- Register a new application in App registrations.
- Add a redirect URI of type Web pointing to
https://your-api.example.com/auth/oidc/callback. - Under Token configuration, add an optional groups claim — choose either security groups or directory roles depending on how you want to assign permissions.
- Generate a client secret under Certificates & secrets.
- The discovery URL is
https://login.microsoftonline.com/{tenant-id}/v2.0. - Note: Azure AD returns group object IDs, not display names. Configure
OIDC_ADMIN_GROUPSandOIDC_EDITOR_GROUPSwith the actual GUIDs.
Auth0
-
Create a new Regular Web Application.
-
Set Allowed Callback URLs to
https://your-api.example.com/auth/oidc/callback. -
Under Advanced Settings → OAuth, ensure OIDC Conformant is enabled.
-
To send a
groupsclaim, create an Auth0 Action that adds it to the ID token:exports.onExecutePostLogin = async (event, api) => { api.idToken.setCustomClaim('groups', event.user.app_metadata?.groups || []); }; -
The discovery URL is
https://your-tenant.auth0.com.
Keycloak
- Create a new OpenID Connect client with Access Type: confidential.
- Set Valid Redirect URIs to
https://your-api.example.com/auth/oidc/callback. - Under Client Scopes → Add → roles, add a mapper of type Group Membership with token claim name
groups. - The discovery URL is
https://your-keycloak.example.com/realms/{realm-name}.
Embed mode
The <pulp-engine-editor> custom element supports OIDC tokens through the oidcToken field on the init message:
const editor = document.querySelector('pulp-engine-editor')
editor.init({
apiUrl: 'https://your-api.example.com',
oidcToken: hostOidcIdToken, // valid id_token from your IDP
templateKey: 'invoice',
})
The iframe calls POST /auth/oidc/exchange with the OIDC token, which validates it against the provider JWKS and returns a Pulp Engine session token. The id_token must be issued within the last 5 minutes (replay protection) and the request Origin must match CORS_ALLOWED_ORIGINS.
Silent token refresh
OIDC-authenticated sessions automatically attempt a silent refresh five minutes before the editor token expires. This uses a hidden iframe pointed at /auth/oidc/login?prompt=none&return_url=/oidc-silent-callback.html to re-authenticate without user interaction.
Provider compatibility: silent refresh requires the OIDC provider to support prompt=none AND to allow framing of its authorization endpoint. Some providers (notably Azure AD/Entra ID under certain tenant configurations) set X-Frame-Options: DENY on their authorization endpoint, which causes prompt=none to fail silently inside the iframe. In that case, Pulp Engine falls back to a full re-login when the token expires — the user is redirected to the SSO sign-in screen and (if their provider session is still active) signed back in immediately without re-entering credentials.
Production hardening
When HARDEN_PRODUCTION=true (auto-derived in production), the following OIDC-specific checks are enforced:
OIDC_REDIRECT_URImust use HTTPS — plain-HTTP redirect URIs leak the authorization code to network observers.REQUIRE_HTTPS=trueandTRUST_PROXY=trueare required (existing controls). The PKCE state cookie’sSecureattribute is tied toREQUIRE_HTTPS.
The following security controls are always active:
- PKCE (S256) on every authorization request, plus nonce binding through the full flow.
- Signed HttpOnly cookies for PKCE state (5-minute TTL,
SameSite=Lax). - Single-use completion codes for the callback → editor handoff (30-second TTL, in-memory store with periodic sweep).
- Origin validation on
POST /auth/oidc/exchange— fails closed whenCORS_ALLOWED_ORIGINSis set. - 5-minute freshness check on
id_tokens submitted to/auth/oidc/exchange(replay protection). - Rate limiting on
/auth/oidc/login(30/min),/auth/oidc/exchange(10/min), and/auth/oidc/complete(10/min).
Audit trail
OIDC authentication events appear in the standard audit log:
editor_token_mintedevents from/auth/oidc/callbackcarrydetails.identityMode = "oidc", the verifiedactor(user ID), anddetails.oidcSub(the IDP subject claim).editor_token_mintedevents from/auth/oidc/exchangecarrydetails.identityMode = "oidc-exchange".oidc_provision_conflictevents are emitted when an OIDCsubclaim maps to an existing key-based user — auto-provisioning is refused and the login is denied.
All template, asset, and other mutations performed by OIDC users carry the user’s Pulp Engine ID as the actor field, identical to named-user attribution.
Troubleshooting
”OIDC discovery failed” at startup
The server cannot fetch {OIDC_DISCOVERY_URL}/.well-known/openid-configuration. Check network access from the API host to the IDP, and verify the URL is correct (no trailing slash needed). The server retries with exponential backoff up to 5 attempts, then continues to recheck every 5 minutes. OIDC routes return 503 until discovery succeeds — the rest of the API continues to serve normally.
”OIDC user does not have access” (403 on callback)
The user’s group claims do not match OIDC_ADMIN_GROUPS or OIDC_EDITOR_GROUPS. Check the actual claim values returned by your IDP — for Azure AD, these are object IDs, not display names. Enable debug logging on the API to inspect the claims:
LOG_LEVEL=debug
“OIDC sub maps to existing non-OIDC user”
A user with the same ID as the sanitized OIDC subject already exists in the registry as a key-based user. This is a deliberate guard against account takeover. Either remove the existing user (DELETE /admin/users/:id) or change their ID to avoid the collision.
Silent refresh never succeeds
Check the browser network panel during a refresh attempt. Common causes:
- Provider sets
X-Frame-Options: DENYon the authorization endpoint (Azure AD/Entra ID is the most common — see “Provider compatibility” above). - Provider does not honor
prompt=noneand shows a login UI inside the iframe instead of returning silently. - Provider session has expired — silent refresh requires an active provider session.
In all cases, Pulp Engine falls back to the full re-login flow when the token actually expires. Users are redirected to the SSO sign-in and (if their provider session is still active elsewhere) signed back in immediately.
Auto-provisioned users disappear after restart
EDITOR_USERS_FILE is not set. Auto-provisioned users only persist when the user registry is backed by a file. Set EDITOR_USERS_FILE=/var/pulp-engine/users.json and restart.