Skip to main content

Authentication

Enterprise validates inbound JWTs against your OIDC provider on every request. There is no user database lookup — identity, organization roles, and project memberships all come from the token claims you configure.

If you're an end user looking to obtain a token to call the API, see the general Authentication page. This page is for operators wiring up the IdP.

Stateless model

The backend reads exactly five OIDC-related environment variables:

VariableRequiredDefaultPurpose
OIDC_ISSUERYesProvider base URL. The backend fetches {issuer}/.well-known/openid-configuration at startup and uses the published JWKS to verify token signatures.
OIDC_CLIENT_IDYesClient ID registered with your IdP. Accepted as a valid aud claim.
OIDC_AUDIENCESNoExtra accepted aud values beyond OIDC_CLIENT_ID. Comma-separated.
OIDC_ROLES_CLAIMNorolesPath to the global roles array in the token. Dot-notation supported (e.g., realm_access.roles).
OIDC_PROJECTS_CLAIMNoprojectsPath to the per-project memberships claim.

What the platform reads from a token

Token verification checks:

  • Signature — RS256 against the JWKS published at the discovery endpoint.
  • Issuer — must match OIDC_ISSUER.
  • Audience — must contain OIDC_CLIENT_ID or one of OIDC_AUDIENCES.
  • Expiry — standard exp check.

Claims read:

ClaimPurpose
subUser identifier.
OIDC_ROLES_CLAIM (default roles)Dual purpose — see Roles, groups, and user tasks. Drives organization-level RBAC and BPMN user-task candidate-group matching.
OIDC_PROJECTS_CLAIM (default projects)Per-project memberships. Shape below.

Project membership shape

The projects claim must carry one entry per project the caller has access to:

{
"projects": [
{ "id": "00000000-0000-0000-0000-000000000001", "roles": ["editor"] },
{ "id": "00000000-0000-0000-0000-000000000002", "roles": ["viewer"] }
]
}

Each entry needs:

  • id — the project UUID, matching the id from your provisioning YAML.
  • roles — array of project-level roles (admin, editor, executor, viewer).

The platform also accepts the claim as a JSON-encoded string for providers that don't ship structured claims natively.

Claim-path dot notation

OIDC_ROLES_CLAIM and OIDC_PROJECTS_CLAIM accept dot-notated paths to nested claims. For example, Keycloak ships realm roles under realm_access.roles:

{
"realm_access": {
"roles": ["admin", "user"]
}
}

Setting OIDC_ROLES_CLAIM=realm_access.roles makes the platform traverse that path.

Wiring up a provider

The platform works with any OIDC-compliant IdP as long as you can shape the tokens it issues to match the contract above. The only piece of code tested against a specific provider's claim layout is Keycloak (used by the in-repo mock OIDC service for end-to-end tests), other providers work generically.

For any provider, you need to:

  1. Register a client for the platform (SPA or confidential, as your IdP requires).
  2. Configure a claim named per OIDC_ROLES_CLAIM containing the user's organization roles, or accept the default roles path.
  3. Configure a claim named per OIDC_PROJECTS_CLAIM containing the per-project memberships in the shape above. Most providers expose this through a custom claim mapper — consult their documentation.
  4. Set OIDC_ISSUER and OIDC_CLIENT_ID on the backend.

Keycloak example

Keycloak is the worked example because it's what the in-repo test fixtures use.

# Backend environment
OIDC_ISSUER=https://keycloak.example.com/realms/quantumbpm
OIDC_CLIENT_ID=quantumbpm-backend
OIDC_ROLES_CLAIM=realm_access.roles
OIDC_PROJECTS_CLAIM=projects

Configure a custom claim mapper in Keycloak to populate the projects claim in the documented shape. Realm roles end up under realm_access.roles automatically.

Other providers

For Okta, Auth0, Azure AD, Zitadel, and others, the same contract applies. The specifics — which UI screens, where the claim mapper lives, whether it's "groups" or "roles" by default — vary by provider, consult their documentation, then point OIDC_ROLES_CLAIM and OIDC_PROJECTS_CLAIM at the right paths. If you hit a wall, contact support@quantumbpm.com.

Mock OIDC for local testing

docker-compose.enterprise.yaml includes a mockoidc service (image hub.quantumbpm.com/quantumbpm/mockoidc) that issues tokens in the shape the platform expects. Use it for local development, replace the issuer URL with your real IdP in production.

Roles, groups, and user tasks

There is no separate "groups" claim. The single array at OIDC_ROLES_CLAIM does double duty:

  1. Organization-level RBAC — recognized role names like user, admin, and system-admin drive global permissions (see the role model).
  2. BPMN user-task candidate-group matching — every other entry in that array is treated as an identity-provider group name. When a BPMN user task carries candidateGroups, the platform authorizes a caller to complete or fail it if any entry of their roles claim matches any of the task's candidate groups (alongside the assignee and candidate-user checks).

This means your roles claim is essentially "the union of role names and group names this user has." Mix them freely:

{
"roles": ["user", "approvers", "finance", "ops-oncall"]
}

In that token:

  • user is interpreted as the platform's user org-role.
  • approvers, finance, ops-oncall are treated as identity-provider groups. A user task with candidateGroups: ["approvers"] will accept this caller, even if they aren't executor on the project.

You don't have to declare which entries are roles and which are groups — the platform doesn't distinguish at parse time. Org-level role enforcement matches against the known role names, user-task authorization just checks for any overlap between the caller's roles array and the task's candidate-group list.

The same applies on the SaaS side (Zitadel project roles play the role-and-group role), so user-task models port cleanly between deployments.

The role model

The platform applies access at two levels: organization roles and per-project roles, with a strict hierarchy:

admin  ⊃  editor  ⊃  executor  ⊃  viewer

The model is identical to SaaS — see Authentication → The role model for the full table.

A request that the platform refuses to authorize returns 404 Not Found rather than 403, to avoid confirming whether a project ID exists at all. If you're getting unexpected 404s, the most common cause is a missing or wrong-cased project entry in the user's projects claim.