The backend is a pure OIDC resource server: it never issues, refreshes or stores tokens. Clients obtain access tokens directly from Zitadel and present them as bearer tokens. Authentication answers who the caller is, RBAC answers what the caller may do; both are always enabled.
The frontend is a public OIDC client of the Zitadel project using Authorization Code with PKCE, so no client secret is shared with the browser.
sequenceDiagram
autonumber
participant U as User (browser)
participant FE as Frontend SPA
participant Z as Zitadel
participant BE as Backend
U->>FE: open the application
FE->>Z: authorization request (PKCE, code_challenge)
Z->>U: login page
U->>Z: credentials (+ MFA)
Z->>FE: redirect with ?code
FE->>Z: token request (code + code_verifier)
Z->>FE: access token (RS256)
FE->>BE: API request, Authorization: Bearer <token>
BE->>BE: verify signature, issuer, audience, expiry
BE->>FE: 200 / 401 / 403
- Algorithm — only asymmetric algorithms the provider advertises and the library supports
(
RS256with Zitadel) are accepted;noneand HMAC tokens are rejected. - OIDC (
coreos/go-oidc) — the discovery document and JWKS are fetched at startup and refreshed when an unknownkidappears, then signature,iss,audandexpare checked. - Subject — a token without
subis rejected;subis the user identity (not an e-mail). - Roles — role names are read from the Zitadel roles claims: the generic
urn:zitadel:iam:org:project:rolesclaim, the legacygroupsclaim a service identity carries and any project scoped roles claim; only names configured throughSD_RBAC_ROLES_*are considered. - Audit logging — every outcome is logged as
auth_auditwithaction,result,idp_type,usernameandreason, in a SIEM-friendly structure.
| Middleware | No token | Invalid token | Effect |
|---|---|---|---|
AuthenticationMW |
401 |
401 |
hard authentication for every write route |
SetJWTClaims |
anonymous | 401 |
soft authentication for read routes |
RBACAuthorizationMW |
– | – | 403 when the token's role names map to no application role |
DenyReporterScopeMW |
– | – | 403 when the caller resolves to the reporter role |
CheckEventExistenceMW |
– | – | 404 for an unknown :eventID before the handler runs |
One deliberate exception: POST /v2/events omits DenyReporterScopeMW because reporting system
incidents is the one write a reporter may perform.
OIDC is mandatory — the application fails to start when it is missing or inconsistent.
| Variable | Required | Default | Notes |
|---|---|---|---|
SD_OIDC_ISSUER |
yes | – | validated together with SD_OIDC_CLIENT_ID; the discovered issuer must match exactly |
SD_OIDC_CLIENT_ID |
yes | – | the audience accepted in aud |
SD_OIDC_USERNAME_CLAIM |
no | – | display name only, never used for identity |
SD_RBAC_ROLES_ADMINS |
yes | – | |
SD_RBAC_ROLES_OPERATORS |
no | – | |
SD_RBAC_ROLES_CREATORS |
no | – | |
SD_RBAC_ROLES_REPORTERS |
no | – |
Each SD_RBAC_ROLES_* variable accepts one role name or a comma-separated list of role names, all
mapped to the same application role. Names are matched case-sensitively and a leading / is ignored
(Zitadel writes project roles as /<project>/<role> in some setups).
SD_RBAC_ROLES_ADMINS=sd_admins,status-dashboard| Role | Priority | Scope |
|---|---|---|
admin |
50 | everything; reserved for future system-level privileges |
operator |
30 | event admin: unrestricted access to all maintenance events |
creator |
10 | create and manage own maintenance events |
reporter |
5 | machine principal: only system incidents, read-only everywhere else |
The highest mapped role wins when a token carries several role names, so a principal with both a
reporter and a creator name behaves as a creator. reporter is deliberately not a step on the
privilege ladder but a machine-only scope.
| Role | maintenance |
incident |
incident with system: true |
info |
|---|---|---|---|---|
creator |
pending_review |
yes | yes | yes |
operator / admin |
planned |
yes | yes | yes |
reporter |
403 |
403 |
yes | 403 |
A caller whose role names map to nothing is rejected with 403 before the handler runs.
| Type | Rule |
|---|---|
maintenance |
operator/admin unrestricted; creator only on own events, only from pending_review and only to pending_review or cancelled |
incident |
any role from creator up; the constraints are the incident state machine, see events.md |
info |
any role from creator up, no ownership check |
For maintenance events version is required (400 when it is missing) and a stale value yields
409; other types also honour the version when it is sent.
Any role except reporter.
Requires operator or admin. A creator receives 403.
| Field | Anonymous | Authenticated |
|---|---|---|
creator, contact_email, version |
hidden | visible for creator role and above |
pending_review / reviewed maintenance |
hidden (404) | visible |
| maintenance cancelled before reaching a public status | hidden (404) | visible |
pending_review / reviewed entries inside updates |
filtered out | visible |
Machine principals holding only the reporter role are treated as anonymous for reads, and the
extended view applies to GET /v2/events, GET /v2/events/:eventID and their deprecated
/v2/incidents aliases. The RSS feeds always use the public view.
incident and info events are never hidden, whatever their status.
| Code | Condition |
|---|---|
400 |
invalid body, missing required field, malformed e-mail, end_date before start_date |
401 |
missing or invalid token |
403 |
no mapped role, insufficient role, foreign event, reporter outside POST /v2/events |
404 |
unknown event, or an event hidden from the caller |
409 |
transition not allowed for the current state, or a stale version |
500 |
database or unexpected server error |