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 — Keycloak, Okta, Auth0, Azure AD, Zitadel, etc. The platform doesn't run its own identity infrastructure; it consumes whatever you already use.

The deployment-time operator configures the issuer URL and the claim paths the platform reads (roles claim, projects claim). For end users — including SDK and worker authors — the contract is the same as SaaS: get a token from your IdP, send it as Authorization: Bearer <token>.

If you're standing up a self-hosted instance, see the Enterprise deployment guide for the configuration options. If you're just using an Enterprise deployment, ask your operator for:

  • The issuer URL and token endpoint of your IdP.
  • A client ID / secret for service-account use, or instructions for getting a personal token.

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