jor
jor6d ago

Scoping to specific organization in JWT token

Hi there, I'm currently running Zitadel 4.2.7 self-hosted as the main authentication platform for a SaaS application. The application consists of a single-page frontend and a backend server. I have configured the ZItadel instance as follows: * Under the default organization, MainCorp, internal users are added. This is also where a project and clients that are used by the application are defined - one of the clients is for the web authentication flow. * Internal users have the role corp:admin, which is a special role that allows for admin tasks in the application, but nothing specific for customer organizations. * For customers, we create a new organization, and grant the MainCorp project to them. When we go to add a user for the customer organization, we can add the role org:admin or org:user to them. This will allow the customer to sign in and use the application. How this is implemented is that upon sign-in, the frontend app specifies the web profile email offline_access urn:zitadel:iam:org:project:id:zitadel:aud scope, and gets a JWT token with a urn:zitadel:iam:org:project:roles key on successful authentication. At present the application doesn't care about multiple projects or client applications, as we only have the one defined. The token is then used in backend requests, and after verifying the signature, the backend will determine the organization to use and available roles from the first organization key within the specific role. This currently works since we don't grant more than one organization to a user. The reason I have done this is because, with short expiry times on tokens, this means the backend does not need to contact the Zitadel instance on every request to verify the token, nor store any secrets or private keys (except for the JWT public key store which can be downloaded from Zitadel and updated periodically if required). (cont'd)
5 Replies
Ask Inkeep
Ask Inkeep6d ago
Hi @jor, I'm an AI assistant for ZITADEL. While a team member reviews this question, I'll see if I can find relevant sources for your question. Answer Looking at your setup, you're using a solid approach with JWT tokens containing role information. Your current implementation with urn:zitadel:iam:org:project:roles is working correctly for single-organization users (1) . For better organization-specific role handling, you should consider using the more specific role scope format. The documentation recommends using urn:zitadel:iam:org:project:{projectId}:roles rather than the general urn:zitadel:iam:org:project:roles . This format includes the specific project ID and represents the more recent model. When you request tokens with the scope urn:zitadel:iam:org:project:id:{projectId}:aud urn:iam:org:project:roles urn:zitadel:iam:org:projects:roles, you'll get role information structured like this in your JWT :
{
"urn:zitadel:iam:org:project:223281986649719041:roles": {
"cfo": {
"223281939119866113": "corporate.user-authorizations-io8epz.zitadel.cloud"
},
"corporate member": {
"223279178798072065": "org-a.user-authorizations-io8epz.zitadel.cloud",
"223279223391912193": "org-b.user-authorizations-io8epz.zitadel.cloud"
}
}
}
{
"urn:zitadel:iam:org:project:223281986649719041:roles": {
"cfo": {
"223281939119866113": "corporate.user-authorizations-io8epz.zitadel.cloud"
},
"corporate member": {
"223279178798072065": "org-a.user-authorizations-io8epz.zitadel.cloud",
"223279223391912193": "org-b.user-authorizations-io8epz.zitadel.cloud"
}
}
}
The role structure shows the organization ID as the value within each role, which allows your backend to determine both the user's roles and which organization they belong to from the JWT token alone. For your multi-tenant setup, you can also configure role settings in the ZITADEL Console by selecting 'User Roles Inside ID Token' in your application settings, or include the claims urn:iam:org:project:roles or urn:zitadel:iam:org:projects:roles in your scope . Your approach of verifying JWT signatures locally without contacting ZITADEL on every request is efficient and well-suited for your architecture. ​ To learn more, see the sources I considered: [1] Retrieve User Roles in ZITADEL ​ Feel free to tag @Ask Inkeep with additional questions.
jor
jorOP6d ago
However, we now have the need for internal users to be able to log in as a specific organization. I know this can be achieved with grants, but the issue is that the token's roles will then contain every organization the user is granted roles on. This could result in a gigantic token, and the application would not be able to determine the "primary" organization the token (and by extension user) is for. Therefore, is it possible to generate or impersonate a token that only contains roles for a specific organization? @Ask Inkeep
Ask Inkeep
Ask Inkeep6d ago
Thanks for the follow-up, @jor. I'll get back to you soon. Answer Yes, you can achieve this using Token Exchange for impersonation in ZITADEL (1) . Token Exchange is a beta feature that allows you to exchange tokens to a different scope, audience, or subject (2) . To enable Token Exchange, you'll first need to enable the feature on your instance (1) :
curl -L -X PUT '<https://$CUSTOM-DOMAIN/v2/features/instance'> \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <IAM_OWNER_TOKEN>' \
--data-raw '{
"oidcTokenExchange": true
}'
curl -L -X PUT '<https://$CUSTOM-DOMAIN/v2/features/instance'> \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <IAM_OWNER_TOKEN>' \
--data-raw '{
"oidcTokenExchange": true
}'
For your use case, you can use Token Exchange to reduce the scope and audience of tokens (2) . Here's how you could exchange a token to limit it to a specific organization:
curl -L -X POST '<http://localhost:9000/oauth/v2/token'> \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'Accept: application/json' \
-u '259254409320529922@portal:eNdXJzB5RK5CXSpa4HqEfbdDqlM7drpskEHq1RBYMby0tM1MaCidyWsWlp5mglbN' \
-d 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
-d 'subject_token=NaUAPHy5mLFQlwUCeUGYeDyhcQYuNhzTiYgwMor9BxP_bfMy2iDdLxJ87nntUc85vNyeHOY' \
-d 'subject_token_type=urn:ietf:params:oauth:token-type:access_token' \
-d 'scope=openid' \
-d 'audience=259254020357488642' | jq
curl -L -X POST '<http://localhost:9000/oauth/v2/token'> \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'Accept: application/json' \
-u '259254409320529922@portal:eNdXJzB5RK5CXSpa4HqEfbdDqlM7drpskEHq1RBYMby0tM1MaCidyWsWlp5mglbN' \
-d 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
-d 'subject_token=NaUAPHy5mLFQlwUCeUGYeDyhcQYuNhzTiYgwMor9BxP_bfMy2iDdLxJ87nntUc85vNyeHOY' \
-d 'subject_token_type=urn:ietf:params:oauth:token-type:access_token' \
-d 'scope=openid' \
-d 'audience=259254020357488642' | jq
The Token Exchange allows you to reduce both scope and audience of the supplied token . When audience is supplied in the request, it may never contain an audience which was not already present in either the subject_token or actor_token combined . This prevents applications from one project or organization from authorizing themselves access to applications of another project or organization . You'll need to configure your application with the urn:ietf:params:oauth:grant-type:token-exchange grant type (1) . ZITADEL recommends only configuring confidential clients with the Token Exchange grant type due to the trust placed in the application . When you need to include roles for a specific organization in the token exchange, you'll need to ensure the appropriate role scopes like urn:zitadel:iam:org:project:id:{projectId}:aud are included (3) . ​ To learn more, see the sources I considered: [1] Impersonation and delegation using Token Exchange [2] Impersonation and delegation using Token Exchange [3] [Bug]: No user roles in access token when doing impersonation ​ Feel free to tag @Ask Inkeep with additional questions.
jor
jorOP6d ago
I think I have found a solution. When supplying urn:zitadel:iam:user:resourceowner:id as a scope during initial authentication, the JWT token will contain the owning organization ID. In the token exchange request, the urn:zitadel:iam:org:id:<org id> and urn:zitadel:iam:org:roles:id:<org id> scopes can be supplied to set the urn:zitadel:iam:org:id property, so the backend can know which organization to use and the roles the user has. However, a valid token can still be generated for an organization the user does not have a grant for, but it will not contain any roles. @Ask Inkeep When initially authenticating the user, is it possible to obtain a JWT token containing only the roles for the user's organization, without knowing the organization ID in advance?
Ask Inkeep
Ask Inkeep6d ago
Thanks for the follow-up, @jor. I'll get back to you soon. Answer I wasn't able to find a direct answer, here are some relevant sources: 1. Impersonation and delegation using Token Exchange : This covers token exchange functionality that allows exchanging tokens to different scopes and audiences, but doesn't specifically address obtaining organization-scoped roles during initial authentication. 2. According to RFC 8693 "scope" is supposed to be returned in JWT Access token : This discusses scope claims in JWT tokens but notes that access tokens configured as JWT (not opaque) should have the scope claim, though it doesn't address organization-specific role filtering during initial authentication. Feel free to tag @Ask Inkeep with additional questions. Glad I could be helpful. Feel free to create a new thread with any new questions you may have.

Did you find this page helpful?