Skip to main content

Authentication

Every QuantumBPM API endpoint requires a bearer token in the Authorization header. The platform doesn't ship its own identity store - tokens are issued by an OpenID Connect (OIDC) provider, and the backend validates them on every request.

This page is the integrator's reference: how to get a token, how to use it, and what role/permission model is applied to your calls.

How auth works​

┌──────────────┐   client credentials    ┌─────────────────┐
│ Client │ ──────────────────────► │ OIDC provider │
│ (worker / │ │ (Zitadel / │
│ SDK / app) │ ◄────── access token ── │ your IdP) │
└──────────────┘ └─────────────────┘
│
│ Authorization: Bearer <token>
▼
┌──────────────────────────────────────┐
│ Platform API │
│ • Validates the token's signature │
│ • Reads identity, roles, projects │
│ from token claims │
│ • Applies project RBAC │
└──────────────────────────────────────┘

The token's claims tell the platform who the caller is, which projects they can access, and at what role. The backend never makes its own decision about permissions - it trusts what your IdP signs.

SaaS​

The managed SaaS uses Zitadel as its identity provider. Manage users, service accounts, and identity providers from the Auth Console (auth.quantumbpm.com).

Two kinds of tokens:

Use caseHow to obtain
Interactive userSign in to the Platform Console; the SPA handles the OIDC flow for you. The token in localStorage works for direct API calls, though it's tied to a browser session and expires
Service account (workers, CI, integrations)Create one in the Auth Console: Service Accounts → New. You'll get a client ID + client secret pair. Use the OAuth 2.0 client credentials grant to mint access tokens

Service accounts are the right answer for anything unattended - workers, scheduled jobs, your backend calling decisions or starting processes. They don't expire on a user session and the role assigned to the service account in the Auth Console maps directly to platform permissions.

Enterprise / self-hosted​

Enterprise builds validate against any standards-compliant OIDC provider - the contract for end users is identical to SaaS. Ask your operator for the issuer URL, the token endpoint, and a client ID / secret (or instructions for getting a personal token).

Operators wiring up the IdP: see Enterprise → Authentication.

Getting a token​

For a service account using the OAuth 2.0 client credentials grant:

curl -X POST "$ISSUER/oauth/v2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=$CLIENT_ID" \
-d "client_secret=$CLIENT_SECRET" \
-d "scope=openid profile"

The response carries an access_token and an expires_in (in seconds). Cache the token until shortly before it expires, then mint a new one. Don't request a fresh token on every API call - your IdP will rate-limit you and your latency will suffer.

{
"access_token": "eyJ…",
"token_type": "Bearer",
"expires_in": 3600
}

The exact token endpoint URL varies by provider. Zitadel uses /oauth/v2/token; Keycloak uses /realms/{realm}/protocol/openid-connect/token; consult your IdP's documentation.

Using the token​

Send the token in the Authorization header on every API call:

curl "$API/projects/$PROJECT/bpmn/instances" \
-H "Authorization: Bearer $TOKEN"

A few practical points:

  • Refresh before expiry, not after. If you wait until you get a 401, in-flight calls fail. Refresh when the token has, say, a minute left.
  • Tokens are bearer credentials. Anyone who has the token can act as you until it expires. Don't log them, don't commit them, and rotate the underlying client secret regularly.
  • One token per service account is fine. Multiple workers can share the same token - the IdP doesn't care, and the platform only reads claims from it.

The role model​

The platform applies access checks at two levels.

Organisation-level roles​

Set on the user / service account in your IdP. Surface as global roles:

RoleGranted byEffect
userDefaultCan sign in and access projects they're explicitly members of
adminOrg adminManage the organisation; not the same as project admin
system-adminOperator onlyImplicit access to every project. Reserved for platform operators

Project-level roles​

Set per project, per member. Strict hierarchy - each role implicitly carries everything below it:

admin  ⊃  editor  ⊃  executor  ⊃  viewer
RoleCan do
viewerRead decisions and processes; view executions and instances
executorEverything viewer can do, plus: call decisions via API, start instances, complete user tasks they're assigned, complete external jobs
editorEverything executor can do, plus: create and modify decisions, processes, and resources
adminEverything editor can do, plus: manage project members and settings

For SaaS, project membership and roles are managed from the Platform Console (Project Settings → Members). For Enterprise, project memberships come from token claims - see the Enterprise deployment docs.

Where the role applies​

Every endpoint requires a specific minimum project role. The most common shapes:

  • Read endpoints (list / get instances, jobs, user tasks, definitions) - viewer for most, executor for /user-tasks to keep the surface tight.
  • Action endpoints (start instance, send message/signal, complete user task, complete external job) - executor. With one exception: a user task's own assignee / candidateUsers / candidateGroups member can complete or fail it without executor - the assignment counts as authorisation. See User tasks → Access control.
  • Authoring endpoints (create / update / delete / deploy resources) - editor.
  • Project administration (members, settings) - admin.

Why a deny looks like a 404​

When the platform refuses to surface a project to a caller - either because the caller doesn't have access at all, or because their role is too low for a specific action - it returns 404 Not Found rather than 403 Forbidden.

This is intentional: it avoids confirming whether a project ID exists at all. If you're getting a 404 you didn't expect, the most common causes are:

  • The caller's token has no membership claim for that project.
  • The caller's role on the project is below what the endpoint requires.
  • The project ID is genuinely wrong.

Check your token's claims (decode it at jwt.io for a quick look) - the project list and roles will be in there.

Token expiry and clock skew​

Two failure modes that usually trip up first-time integrators:

SymptomLikely cause
401 Unauthorized on a previously-working tokenToken expired. Mint a new one
401 Unauthorized on a freshly-minted tokenServer clock is ahead of your IdP's. Tokens carry an iat (issued-at) claim that the validator rejects when it's slightly in the future. Sync time on both ends, or adjust the IdP's clock-skew tolerance
Sporadic 401s under loadMultiple workers minting and immediately invalidating each others' tokens - make sure each service account uses its own token, cached locally

Where it shows up next​