Pulp Engine Document Rendering
Get started

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

  1. Register Pulp Engine as a confidential OAuth client with your identity provider.

  2. Configure the redirect URI as https://your-api.example.com/auth/oidc/callback.

  3. 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
  4. Restart the API. The editor login screen will show a “Sign in with SSO” button.

Configuration reference

Required when OIDC is enabled

VariableDescription
OIDC_DISCOVERY_URLIssuer URL — the server fetches {url}/.well-known/openid-configuration at startup.
OIDC_CLIENT_IDConfidential client ID registered with the OIDC provider.
OIDC_CLIENT_SECRETConfidential client secret.
OIDC_REDIRECT_URIAbsolute callback URL. Must match what is registered with the provider. Must use HTTPS in hardened production mode.
OIDC_COOKIE_SECRET32+ character secret used to sign the short-lived PKCE state cookie. Generate with openssl rand -hex 32.

Optional

VariableDefaultDescription
OIDC_SCOPESopenid profile emailSpace-separated OAuth scopes to request.
OIDC_CLAIM_SUBsubClaim used as the OIDC subject identifier.
OIDC_CLAIM_EMAILemailClaim used to populate user email.
OIDC_CLAIM_DISPLAY_NAMEnameClaim used as the user’s display name.
OIDC_CLAIM_GROUPSgroupsClaim 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_PROVISIONtrueWhen true, new users are auto-created in the user registry on first OIDC login. Requires EDITOR_USERS_FILE for persistence across restarts.
OIDC_PROVIDER_NAMESSODisplay name for the SSO button in the editor.

Authorization model

OIDC users are mapped to Pulp Engine scopes through group claims:

  1. The server reads the configured OIDC_CLAIM_GROUPS claim from the verified ID token.
  2. If any value matches OIDC_ADMIN_GROUPS, the user receives admin scope.
  3. Otherwise, if any value matches OIDC_EDITOR_GROUPS (or OIDC_EDITOR_GROUPS=*), the user receives editor scope.
  4. 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 OIDC sub claim (sanitized to [a-zA-Z0-9_-], truncated to 64 chars)
  • displayName: from the configured display name claim
  • oidcSub: the original sub claim
  • authMethod: '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

MethodPathPurpose
GET/auth/oidc/loginRedirects the browser to the OIDC authorization endpoint with PKCE + nonce.
GET/auth/oidc/callbackHandles the provider redirect, exchanges the code for tokens, mints a Pulp Engine session, redirects with a one-time completion code.
POST/auth/oidc/completeExchanges a single-use completion code for a Pulp Engine editor token.
POST/auth/oidc/exchangeEmbed mode: exchanges an OIDC id_token directly for a Pulp Engine editor token.
GET/auth/oidc/logoutRP-initiated logout — redirects to the provider’s end_session_endpoint.

Provider-specific setup

Okta

  1. Create a new OIDC application of type Web.

  2. Set the sign-in redirect URI to https://your-api.example.com/auth/oidc/callback.

  3. Under Sign On → OpenID Connect ID Token, ensure groups claim is included. Add a claim filter (e.g. Matches regex: .*) to send all groups, or scope it.

  4. Note the issuer (typically https://your-org.okta.com), client ID, and client secret.

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

  1. Register a new application in App registrations.
  2. Add a redirect URI of type Web pointing to https://your-api.example.com/auth/oidc/callback.
  3. Under Token configuration, add an optional groups claim — choose either security groups or directory roles depending on how you want to assign permissions.
  4. Generate a client secret under Certificates & secrets.
  5. The discovery URL is https://login.microsoftonline.com/{tenant-id}/v2.0.
  6. Note: Azure AD returns group object IDs, not display names. Configure OIDC_ADMIN_GROUPS and OIDC_EDITOR_GROUPS with the actual GUIDs.

Auth0

  1. Create a new Regular Web Application.

  2. Set Allowed Callback URLs to https://your-api.example.com/auth/oidc/callback.

  3. Under Advanced Settings → OAuth, ensure OIDC Conformant is enabled.

  4. To send a groups claim, 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 || []);
    };
  5. The discovery URL is https://your-tenant.auth0.com.

Keycloak

  1. Create a new OpenID Connect client with Access Type: confidential.
  2. Set Valid Redirect URIs to https://your-api.example.com/auth/oidc/callback.
  3. Under Client Scopes → Add → roles, add a mapper of type Group Membership with token claim name groups.
  4. 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_URI must use HTTPS — plain-HTTP redirect URIs leak the authorization code to network observers.
  • REQUIRE_HTTPS=true and TRUST_PROXY=true are required (existing controls). The PKCE state cookie’s Secure attribute is tied to REQUIRE_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 when CORS_ALLOWED_ORIGINS is 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_minted events from /auth/oidc/callback carry details.identityMode = "oidc", the verified actor (user ID), and details.oidcSub (the IDP subject claim).
  • editor_token_minted events from /auth/oidc/exchange carry details.identityMode = "oidc-exchange".
  • oidc_provision_conflict events are emitted when an OIDC sub claim 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: DENY on the authorization endpoint (Azure AD/Entra ID is the most common — see “Provider compatibility” above).
  • Provider does not honor prompt=none and 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.