diff --git a/docs/docs/apis/openidoauth/claims.md b/docs/docs/apis/openidoauth/claims.md index ab3f120b425..c82e3a3883b 100644 --- a/docs/docs/apis/openidoauth/claims.md +++ b/docs/docs/apis/openidoauth/claims.md @@ -35,7 +35,6 @@ Please check below the matrix for an overview where which scope is asserted. | sub | Yes | Yes | Yes | When JWT | | urn:zitadel:iam:org:domain:primary:\{domainname} | When requested | When requested | When requested | When JWT and requested | | urn:zitadel:iam:org:project:roles | When requested | When requested | When requested or configured | When JWT and requested or configured | -| urn:zitadel:iam:org:projects:roles | When requested | When requested | When requested or configured | When JWT and requested or configured | | urn:zitadel:iam:user:metadata | When requested | When requested | When requested | When JWT and requested | | urn:zitadel:iam:user:resourceowner:id | When requested | When requested | When requested | When JWT and requested | | urn:zitadel:iam:user:resourceowner:name | When requested | When requested | When requested | When JWT and requested | @@ -110,7 +109,6 @@ ZITADEL reserves some claims to assert certain data. Please check out the [reser | urn:zitadel:iam:action:\{actionname}:log | `{"urn:zitadel:iam:action:appendCustomClaims:log": ["test log", "another test log"]}` | This claim is set during Actions as a log, e.g. if two custom claims with the same keys are set. | | urn:zitadel:iam:org:domain:primary:\{domainname} | `{"urn:zitadel:iam:org:domain:primary": "acme.ch"}` | This claim represents the primary domain of the organization the user belongs to. | | urn:zitadel:iam:org:project:roles | `{"urn:zitadel:iam:org:project:roles": [ {"user": {"id1": "acme.zitade.ch", "id2": "caos.ch"} } ] }` | When roles are asserted, ZITADEL does this by providing the `id` and `primaryDomain` below the role. This gives you the option to check in which organization a user has the role on the current project (where your client belongs to). | -| urn:zitadel:iam:org:projects:roles | `{"urn:zitadel:iam:org:projects:roles": [ {"user": {"id1": "acme.zitade.ch", "id2": "caos.ch"} } ] }` | When roles are asserted, ZITADEL provides the `id` and `primaryDomain` for each role. This allows you to identify the organizations where a user has roles across all requested projects by audience. | | urn:zitadel:iam:org:project:\{projectid}:roles | `{"urn:zitadel:iam:org:project:id3:roles": [ {"user": {"id1": "acme.zitade.ch", "id2": "caos.ch"} } ] }` | When roles are asserted, ZITADEL does this by providing the `id` and `primaryDomain` below the role. This gives you the option to check in which organization a user has the role on a specific project. | | urn:zitadel:iam:user:metadata | `{"urn:zitadel:iam:user:metadata": [ {"key": "VmFsdWU=" } ] }` | The metadata claim will include all metadata of a user. The values are base64 encoded. | | urn:zitadel:iam:user:resourceowner:id | `{"urn:zitadel:iam:user:resourceowner:id": "orgid"}` | This claim represents the user's organization ID. | diff --git a/internal/api/oidc/client.go b/internal/api/oidc/client.go index 045ef4e196d..ced270e5456 100644 --- a/internal/api/oidc/client.go +++ b/internal/api/oidc/client.go @@ -25,7 +25,6 @@ const ( ScopeProjectsRoles = "urn:zitadel:iam:org:projects:roles" ClaimProjectRoles = "urn:zitadel:iam:org:project:roles" ClaimProjectRolesFormat = "urn:zitadel:iam:org:project:%s:roles" - ClaimProjectsRoles = "urn:zitadel:iam:org:projects:roles" ScopeUserMetaData = "urn:zitadel:iam:user:metadata" ClaimUserMetaData = ScopeUserMetaData ScopeResourceOwner = "urn:zitadel:iam:user:resourceowner" @@ -96,7 +95,7 @@ func (o *OPStorage) GetPrivateClaimsFromScopes(context.Context, string, string, func checkGrantedRoles(roles *projectsRoles, grant query.UserGrant, requestedRole string, isRequested bool) { for _, grantedRole := range grant.Roles { if requestedRole == grantedRole { - roles.Add(grant.ProjectID, grantedRole, grant.ResourceOwner, grant.OrgPrimaryDomain, isRequested, false) + roles.Add(grant.ProjectID, grantedRole, grant.ResourceOwner, grant.OrgPrimaryDomain, isRequested) } } } @@ -107,11 +106,9 @@ type projectsRoles struct { projects map[string]projectRoles requestProjectID string - - requestAudIDs map[string]struct{} } -func newProjectRoles(projectID string, grants []query.UserGrant, requestedRoles []string, roleAudience []string) *projectsRoles { +func newProjectRoles(projectID string, grants []query.UserGrant, requestedRoles []string) *projectsRoles { roles := new(projectsRoles) // if specific roles where requested, check if they are granted and append them in the roles list if len(requestedRoles) > 0 { @@ -120,19 +117,18 @@ func newProjectRoles(projectID string, grants []query.UserGrant, requestedRoles checkGrantedRoles(roles, grant, requestedRole, grant.ProjectID == projectID) } } + return roles } // no specific roles were requested, so convert any grants into roles for _, grant := range grants { for _, role := range grant.Roles { - for _, projectAud := range roleAudience { - roles.Add(grant.ProjectID, role, grant.ResourceOwner, grant.OrgPrimaryDomain, grant.ProjectID == projectID, grant.ProjectID == projectAud) - } + roles.Add(grant.ProjectID, role, grant.ResourceOwner, grant.OrgPrimaryDomain, grant.ProjectID == projectID) } } return roles } -func (p *projectsRoles) Add(projectID, roleKey, orgID, domain string, isRequested bool, isAudienceReq bool) { +func (p *projectsRoles) Add(projectID, roleKey, orgID, domain string, isRequested bool) { if p.projects == nil { p.projects = make(map[string]projectRoles, 1) } @@ -142,12 +138,6 @@ func (p *projectsRoles) Add(projectID, roleKey, orgID, domain string, isRequeste if isRequested { p.requestProjectID = projectID } - if p.requestAudIDs == nil { - p.requestAudIDs = make(map[string]struct{}, 1) - } - if isAudienceReq { - p.requestAudIDs[projectID] = struct{}{} - } p.projects[projectID].Add(roleKey, orgID, domain) } diff --git a/internal/api/oidc/integration_test/userinfo_project_roles_test.go b/internal/api/oidc/integration_test/userinfo_project_roles_test.go deleted file mode 100644 index 39906b57689..00000000000 --- a/internal/api/oidc/integration_test/userinfo_project_roles_test.go +++ /dev/null @@ -1,106 +0,0 @@ -//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) - } -} diff --git a/internal/api/oidc/integration_test/userinfo_test.go b/internal/api/oidc/integration_test/userinfo_test.go index 2fce70920d0..b1b63686903 100644 --- a/internal/api/oidc/integration_test/userinfo_test.go +++ b/internal/api/oidc/integration_test/userinfo_test.go @@ -111,7 +111,7 @@ func TestServer_UserInfo(t *testing.T) { assertions: []func(*testing.T, *oidc.UserInfo){ assertUserinfo, func(t *testing.T, ui *oidc.UserInfo) { - assertProjectRoleClaims(t, projectID, ui.Claims, true, []string{roleFoo, roleBar}, []string{Instance.DefaultOrg.Id}) + assertProjectRoleClaims(t, projectID, ui.Claims, true, []string{roleFoo}, []string{Instance.DefaultOrg.Id}) }, }, }, @@ -125,7 +125,7 @@ func TestServer_UserInfo(t *testing.T) { assertions: []func(*testing.T, *oidc.UserInfo){ assertUserinfo, func(t *testing.T, ui *oidc.UserInfo) { - assertProjectRoleClaims(t, projectID, ui.Claims, true, []string{roleFoo, roleBar}, []string{Instance.DefaultOrg.Id}) + assertProjectRoleClaims(t, projectID, ui.Claims, true, []string{roleFoo}, []string{Instance.DefaultOrg.Id}) }, }, }, @@ -267,7 +267,7 @@ func TestServer_UserInfo_Issue6662(t *testing.T) { userinfo, err := rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, user.GetUserId(), provider) require.NoError(t, err) - assertProjectRoleClaims(t, projectID, userinfo.Claims, false, []string{roleFoo, roleBar}, []string{Instance.DefaultOrg.Id}) + assertProjectRoleClaims(t, projectID, userinfo.Claims, false, []string{roleFoo}, []string{Instance.DefaultOrg.Id}) } func addProjectRolesGrants(t *testing.T, userID, projectID string, roles ...string) { diff --git a/internal/api/oidc/userinfo.go b/internal/api/oidc/userinfo.go index 83b806cd285..15ec5b21e02 100644 --- a/internal/api/oidc/userinfo.go +++ b/internal/api/oidc/userinfo.go @@ -209,7 +209,7 @@ func assertRoles(projectID string, user *query.OIDCUserInfo, roleAudience, reque } // prevent returning obtained grants if none where requested if (projectID != "" && len(requestedRoles) > 0) || len(roleAudience) > 0 { - setUserInfoRoleClaims(info, newProjectRoles(projectID, user.UserGrants, requestedRoles, roleAudience)) + setUserInfoRoleClaims(info, newProjectRoles(projectID, user.UserGrants, requestedRoles)) } } @@ -278,25 +278,12 @@ func setUserInfoOrgClaims(user *query.OIDCUserInfo, out *oidc.UserInfo) { func setUserInfoRoleClaims(userInfo *oidc.UserInfo, roles *projectsRoles) { if roles != nil && len(roles.projects) > 0 { - // Create a map to store accumulated roles for ClaimProjectsRoles - projectsRoles := make(projectRoles) - - for requestAudID := range roles.requestAudIDs { - if projectRole, ok := roles.projects[requestAudID]; ok { - maps.Copy(projectsRoles, projectRole) - } - } - if roles, ok := roles.projects[roles.requestProjectID]; ok { userInfo.AppendClaims(ClaimProjectRoles, roles) } for projectID, roles := range roles.projects { userInfo.AppendClaims(fmt.Sprintf(ClaimProjectRolesFormat, projectID), roles) } - // Finally, set the accumulated ClaimProjectsRoles - if len(projectsRoles) > 0 { - userInfo.AppendClaims(ClaimProjectsRoles, projectsRoles) - } } } diff --git a/internal/domain/request.go b/internal/domain/request.go index 5dfad584d52..92e45c0d2f5 100644 --- a/internal/domain/request.go +++ b/internal/domain/request.go @@ -7,7 +7,6 @@ const ( OrgDomainPrimaryClaim = "urn:zitadel:iam:org:domain:primary" OrgIDClaim = "urn:zitadel:iam:org:id" ProjectIDScope = "urn:zitadel:iam:org:project:id:" - ProjectsIDScope = "urn:zitadel:iam:org:projects:roles" ProjectIDScopeZITADEL = "zitadel" AudSuffix = ":aud" ProjectScopeZITADEL = ProjectIDScope + ProjectIDScopeZITADEL + AudSuffix