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:
| Variable | Required | Default | Purpose |
|---|---|---|---|
OIDC_ISSUER | Yes | — | Provider base URL. The backend fetches {issuer}/.well-known/openid-configuration at startup and uses the published JWKS to verify token signatures. |
OIDC_CLIENT_ID | Yes | — | Client ID registered with your IdP. Accepted as a valid aud claim. |
OIDC_AUDIENCES | No | — | Extra accepted aud values beyond OIDC_CLIENT_ID. Comma-separated. |
OIDC_ROLES_CLAIM | No | roles | Path to the global roles array in the token. Dot-notation supported (e.g., realm_access.roles). |
OIDC_PROJECTS_CLAIM | No | projects | Path 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_IDor one ofOIDC_AUDIENCES. - Expiry — standard
expcheck.
Claims read:
| Claim | Purpose |
|---|---|
sub | User 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 theidfrom 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:
- Register a client for the platform (SPA or confidential, as your IdP requires).
- Configure a claim named per
OIDC_ROLES_CLAIMcontaining the user's organization roles, or accept the defaultrolespath. - Configure a claim named per
OIDC_PROJECTS_CLAIMcontaining the per-project memberships in the shape above. Most providers expose this through a custom claim mapper — consult their documentation. - Set
OIDC_ISSUERandOIDC_CLIENT_IDon 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:
- Organization-level RBAC — recognized role names like
user,admin, andsystem-admindrive global permissions (see the role model). - 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:
useris interpreted as the platform'suserorg-role.approvers,finance,ops-oncallare treated as identity-provider groups. A user task withcandidateGroups: ["approvers"]will accept this caller, even if they aren'texecutoron 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.