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.
Wildcard (*) - all-projects grant
A project entry with id set to "*" grants its roles on every project, present and future. This is intended for a platform-operations team that administers the whole BPMN stack:
{
"projects": [
{ "id": "*", "roles": ["admin"] }
]
}
The wildcard's own roles array is honored - "*" with ["admin"] grants admin everywhere, ["viewer"] grants read-only everywhere. An explicit per-project entry takes effect when it grants a higher role than the wildcard (e.g. a ["viewer"] wildcard plus an ["admin"] entry for one project = admin on that project, viewer elsewhere).
Security note: a
"*"entry is a broad grant. Restrict who your IdP maps it to - anyone whose token carries{"id":"*","roles":["admin"]}becomes an administrator of every project.
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
The repo bundles a small mock OIDC provider (tools/mockoidc) that issues tokens in the shape the platform expects. It is a local/CI testing tool only - it hands out a one-click global-admin token and must never be exposed in a real deployment. Run it locally with make run-mock-oidc (or it is built on demand by the kind test harness). In production, point OIDC_ISSUER at your real IdP.
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.