Extend Authorization in ZITADEL with Organization Metadata + preAccessToken Action
Map roles to permissions in your org metadata and append a permissions claim to Access Tokens during the pre-issuance flow.
This guide shows how to enrich Access Tokens with a permissions claim derived from your Organization’s role-to-permissions mapping. You’ll store that mapping in Organization metadata, verify ZITADEL’s webhook signature, read the metadata inside a Function → preAccessToken Action, and return append_claims so the permissions array is issued with every token. We include example metadata shapes, sample responses, and UI/API steps.
Reference implementation: Check our Actions V2 + Cloudflare Workers utility and read the AUTHORIZATION.md guide. It exposes an endpoint to handle role-to-permissions mapping.
Why this pattern?
Many apps authorize on permissions, while ZITADEL natively grants roles on projects/grants. By adding a lightweight Action in the pre access token creation flow, you can translate roles → permissions at token time and deliver a standard permissions claim your API gateways and services already understand. This runs just before token issuance in the PreAccessToken Flow.
Prerequisites
- ZITADEL v3+ with Actions v2 enabled on your instance.
- A service user (PAT or Client Credentials) with access to Organization Service v2 to read metadata. (See API reference).
- A project with users who already hold project roles via user grants.
Step 1 — Model your permissions in Organization Metadata
You’ll store a mapping from role name → array of permissions in Organization metadata. ZITADEL’s v2 Organization API exposes ListOrganizationMetadata / SetOrganizationMetadata.
Recommended shape: Per-role keys (simple & scalable)
Create one metadata entry per role, where the key equals the role name:
- Key: admin → Value (JSON string):
["read:all","write:all","delete:all"] - Key: editor →
["posts:read","posts:write"] - Key: viewer →
["posts:read"]
This keeps updates surgical (change one role value, no collisions) and matches the logic “for each user role, look up a same-named metadata key”.
Where to set it
- Console: Organization → Metadata (paste JSON strings in the Value field).

- API v2: Organization Service → SetOrganizationMetadata.
Step 2 — Create a Target (webhook) and capture the signing key
Create a Target that ZITADEL will call from the Action. The response includes a signingKey that you must use to verify the request signature. Example (docs snippet):
curl -L -X POST "https://$CUSTOM_DOMAIN/v2/actions/targets" \
-H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
--data-raw '{
"name": "Authorization Webhook",
"restCall": { "interruptOnError": false },
"endpoint": "https://<HOSTING_DOMAIN>/",
"timeout": "10s"
}'
# Response (excerpt)
# { "id": "342320645008366333", "signingKey": "<SIGNING_KEY>" }
Keep that signingKey safe; this must be saved as the AUTHORIZATION_SIGNING_KEY environment variable. See the Actions V2 + Cloudflare Workers utility and read the AUTHORIZATION.md guide for more details.
Step 3 — Implement the preAccessToken Action (Function)
This Action runs in the Function execution → preAccessToken condition and can append claims by returning an append_claims array.
What the sample code will do
- Verify signature using the
signingKey. - Parse request and collect the user’s roles (e.g., from
user_grants[].roles) supplied in the preAccessToken payload. - Fetch Organization metadata via ListOrganizationMetadata (v2).
- Map roles → permissions by reading a metadata entry whose key equals the role name.
- Return an
append_claimsobject with a deduplicatedpermissionsarray.
Minimal example response
{
"append_claims": [
{
"key": "permissions",
"value": ["read:all", "write:all"]
}
]
}
That’s all that’s required for ZITADEL to add permissions into the Access Token.
Step 4 — Wire it up in Console
- Go to Console → Actions.
- Create Function → select pre access token creation.
- Choose your Authorization Webhook Target and save.
Step 5 — Test and inspect the token
Use any OIDC client (or ZITADEL’s OIDC Playground) to request an Access Token and then decode the JWT to verify the claim. A minimal vanilla JS app can be used to login and decode the user tokens to demonstrate the final result:Example decoded Access Token payload (excerpt)
{
"iss": "https://auth.example.com",
"aud": [
"259256588127174658",
"257786991247294468"
],
"sub": "259242039378444290",
"permissions": ["read:all", "write:all", "delete:all"],
"exp": 1762730441,
"iat": 1762726841
}