mirror of
https://github.com/zitadel/zitadel.git
synced 2025-11-02 03:38:46 +00:00
# Which Problems Are Solved
The /userinfo endpoint only returns roles for the current project, even
if the access token includes multiple project aud scopes.
This prevents clients from retrieving all user roles across multiple
projects, making multi-project access control ineffective.
# How the Problems Are Solved
Modified the /userinfo handler logic to resolve roles across all valid
project audience scopes provided in the token, not just the current
project.
Ensured that if **urn:zitadel:iam:org:projects:roles is in the scopes**,
roles from all declared project audiences are collected and included in
the response in **urn:zitadel:iam:org:projects:roles claim**.
# Additional Changes
# Additional Context
This change enables service-to-service authorization workflows and SPA
role resolution across multiple project contexts with a single token.
- Closes #9831
---------
Co-authored-by: Masum Patel <patelmasum98@gmail.com>
Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>
(cherry picked from commit 295584648d)
107 lines
3.6 KiB
Go
107 lines
3.6 KiB
Go
//go:build integration
|
|
|
|
package oidc_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
|
|
|
oidc_api "github.com/zitadel/zitadel/internal/api/oidc"
|
|
"github.com/zitadel/zitadel/internal/domain"
|
|
)
|
|
|
|
func TestServer_UserInfo_ProjectsRolesClaim(t *testing.T) {
|
|
const (
|
|
roleFoo = "foo"
|
|
roleBar = "bar"
|
|
roleBaz = "baz"
|
|
)
|
|
|
|
clientID, projectID1 := createClient(t, Instance)
|
|
_, projectID2 := createClient(t, Instance)
|
|
addProjectRolesGrants(t, User.GetUserId(), projectID1, roleFoo, roleBar)
|
|
addProjectRolesGrants(t, User.GetUserId(), projectID2, roleBaz)
|
|
|
|
tests := []struct {
|
|
name string
|
|
prepare func(t *testing.T, clientID string, scope []string) *oidc.Tokens[*oidc.IDTokenClaims]
|
|
scope []string
|
|
assertions []func(*testing.T, *oidc.UserInfo)
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "project role and audience scope with only current project",
|
|
prepare: getTokens,
|
|
scope: []string{oidc.ScopeProfile, oidc.ScopeOpenID, oidc.ScopeEmail, oidc.ScopeOfflineAccess,
|
|
domain.ProjectIDScope + projectID1 + domain.AudSuffix, domain.ProjectsIDScope,
|
|
},
|
|
assertions: []func(*testing.T, *oidc.UserInfo){
|
|
assertUserinfo,
|
|
func(t *testing.T, ui *oidc.UserInfo) {
|
|
assertProjectsRolesClaims(t, ui.Claims, []string{roleFoo, roleBar}, []string{Instance.DefaultOrg.Id})
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "project role and audience scope with multiple projects as audience",
|
|
prepare: getTokens,
|
|
scope: []string{oidc.ScopeProfile, oidc.ScopeOpenID, oidc.ScopeEmail, oidc.ScopeOfflineAccess,
|
|
domain.ProjectIDScope + projectID1 + domain.AudSuffix,
|
|
domain.ProjectIDScope + projectID2 + domain.AudSuffix,
|
|
domain.ProjectsIDScope,
|
|
},
|
|
assertions: []func(*testing.T, *oidc.UserInfo){
|
|
assertUserinfo,
|
|
func(t *testing.T, ui *oidc.UserInfo) {
|
|
assertProjectsRolesClaims(t, ui.Claims, []string{roleFoo, roleBar, roleBaz}, []string{Instance.DefaultOrg.Id})
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tokens := tt.prepare(t, clientID, tt.scope)
|
|
provider, err := Instance.CreateRelyingParty(CTX, clientID, redirectURI)
|
|
require.NoError(t, err)
|
|
userinfo, err := rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
for _, assertion := range tt.assertions {
|
|
assertion(t, userinfo)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// assertProjectsRoleClaims asserts the projectRoles in the claims.
|
|
func assertProjectsRolesClaims(t *testing.T, claims map[string]any, wantRoles, wantRoleOrgIDs []string) {
|
|
t.Helper()
|
|
projectsRoleClaims := make([]string, 0, 2)
|
|
projectsRoleClaims = append(projectsRoleClaims, oidc_api.ClaimProjectsRoles)
|
|
for _, claim := range projectsRoleClaims {
|
|
roleMap, ok := claims[claim].(map[string]any) // map of multiple roles
|
|
require.Truef(t, ok, "claim %s not found or wrong type %T", claim, claims[claim])
|
|
|
|
gotRoles := make([]string, 0, len(roleMap))
|
|
for roleKey := range roleMap {
|
|
role, ok := roleMap[roleKey].(map[string]any) // map of multiple org IDs to org domains
|
|
require.Truef(t, ok, "role %s not found or wrong type %T", roleKey, roleMap[roleKey])
|
|
gotRoles = append(gotRoles, roleKey)
|
|
|
|
gotRoleOrgIDs := make([]string, 0, len(role))
|
|
for orgID := range role {
|
|
gotRoleOrgIDs = append(gotRoleOrgIDs, orgID)
|
|
}
|
|
assert.ElementsMatch(t, wantRoleOrgIDs, gotRoleOrgIDs)
|
|
}
|
|
assert.ElementsMatch(t, wantRoles, gotRoles)
|
|
}
|
|
}
|