mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-15 04:18:01 +00:00
6a51c4b0f5
* feat(oidc): optimize the userinfo endpoint
* store project ID in the access token
* query for projectID if not in token
* add scope based tests
* Revert "store project ID in the access token"
This reverts commit 5f0262f239
.
* query project role assertion
* use project role assertion setting to return roles
* workaround eventual consistency and handle PAT
* do not append empty project id
271 lines
9.0 KiB
Go
271 lines
9.0 KiB
Go
//go:build integration
|
|
|
|
package oidc_test
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"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"
|
|
"golang.org/x/oauth2"
|
|
|
|
oidc_api "github.com/zitadel/zitadel/internal/api/oidc"
|
|
"github.com/zitadel/zitadel/internal/integration"
|
|
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
|
|
"github.com/zitadel/zitadel/pkg/grpc/management"
|
|
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
|
|
)
|
|
|
|
// TestServer_UserInfo is a top-level test which re-executes the actual
|
|
// userinfo integration test against a matrix of different feature flags.
|
|
// This ensure that the response of the different implementations remains the same.
|
|
func TestServer_UserInfo(t *testing.T) {
|
|
iamOwnerCTX := Tester.WithAuthorization(CTX, integration.IAMOwner)
|
|
t.Cleanup(func() {
|
|
_, err := Tester.Client.FeatureV2.ResetInstanceFeatures(iamOwnerCTX, &feature.ResetInstanceFeaturesRequest{})
|
|
require.NoError(t, err)
|
|
})
|
|
tests := []struct {
|
|
name string
|
|
legacy bool
|
|
trigger bool
|
|
}{
|
|
{
|
|
name: "legacy enabled",
|
|
legacy: true,
|
|
},
|
|
{
|
|
name: "legacy disabled, trigger disabled",
|
|
legacy: false,
|
|
trigger: false,
|
|
},
|
|
{
|
|
name: "legacy disabled, trigger enabled",
|
|
legacy: false,
|
|
trigger: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, err := Tester.Client.FeatureV2.SetInstanceFeatures(iamOwnerCTX, &feature.SetInstanceFeaturesRequest{
|
|
OidcLegacyIntrospection: &tt.legacy,
|
|
OidcTriggerIntrospectionProjections: &tt.trigger,
|
|
})
|
|
require.NoError(t, err)
|
|
testServer_UserInfo(t)
|
|
})
|
|
}
|
|
}
|
|
|
|
// testServer_UserInfo is the actual userinfo integration test,
|
|
// which calls the userinfo endpoint with different client configurations, roles and token scopes.
|
|
func testServer_UserInfo(t *testing.T) {
|
|
const role = "testUserRole"
|
|
clientID, projectID := createClient(t)
|
|
_, err := Tester.Client.Mgmt.AddProjectRole(CTX, &management.AddProjectRoleRequest{
|
|
ProjectId: projectID,
|
|
RoleKey: role,
|
|
DisplayName: "test",
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = Tester.Client.Mgmt.AddUserGrant(CTX, &management.AddUserGrantRequest{
|
|
UserId: User.GetUserId(),
|
|
ProjectId: projectID,
|
|
RoleKeys: []string{role},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
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: "invalid token",
|
|
prepare: func(*testing.T, string, []string) *oidc.Tokens[*oidc.IDTokenClaims] {
|
|
return &oidc.Tokens[*oidc.IDTokenClaims]{
|
|
Token: &oauth2.Token{
|
|
AccessToken: "DEAFBEEFDEADBEEF",
|
|
TokenType: oidc.BearerToken,
|
|
},
|
|
IDTokenClaims: &oidc.IDTokenClaims{
|
|
TokenClaims: oidc.TokenClaims{
|
|
Subject: User.GetUserId(),
|
|
},
|
|
},
|
|
}
|
|
},
|
|
scope: []string{oidc.ScopeProfile, oidc.ScopeOpenID, oidc.ScopeEmail, oidc.ScopeOfflineAccess},
|
|
assertions: []func(*testing.T, *oidc.UserInfo){
|
|
func(t *testing.T, ui *oidc.UserInfo) {
|
|
assert.Nil(t, ui)
|
|
},
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "standard scopes",
|
|
prepare: getTokens,
|
|
scope: []string{oidc.ScopeProfile, oidc.ScopeOpenID, oidc.ScopeEmail, oidc.ScopeOfflineAccess},
|
|
assertions: []func(*testing.T, *oidc.UserInfo){
|
|
assertUserinfo,
|
|
func(t *testing.T, ui *oidc.UserInfo) {
|
|
assertNoReservedScopes(t, ui.Claims)
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "project role assertion",
|
|
prepare: func(t *testing.T, clientID string, scope []string) *oidc.Tokens[*oidc.IDTokenClaims] {
|
|
_, err := Tester.Client.Mgmt.UpdateProject(CTX, &management.UpdateProjectRequest{
|
|
Id: projectID,
|
|
Name: fmt.Sprintf("project-%d", time.Now().UnixNano()),
|
|
ProjectRoleAssertion: true,
|
|
})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
_, err := Tester.Client.Mgmt.UpdateProject(CTX, &management.UpdateProjectRequest{
|
|
Id: projectID,
|
|
Name: fmt.Sprintf("project-%d", time.Now().UnixNano()),
|
|
ProjectRoleAssertion: false,
|
|
})
|
|
require.NoError(t, err)
|
|
})
|
|
resp, err := Tester.Client.Mgmt.GetProjectByID(CTX, &management.GetProjectByIDRequest{Id: projectID})
|
|
require.NoError(t, err)
|
|
require.True(t, resp.GetProject().GetProjectRoleAssertion(), "project role assertion")
|
|
|
|
return getTokens(t, clientID, scope)
|
|
},
|
|
scope: []string{oidc.ScopeProfile, oidc.ScopeOpenID, oidc.ScopeEmail, oidc.ScopeOfflineAccess},
|
|
assertions: []func(*testing.T, *oidc.UserInfo){
|
|
assertUserinfo,
|
|
func(t *testing.T, ui *oidc.UserInfo) {
|
|
assertProjectRoleClaims(t, projectID, ui.Claims, role)
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "projects roles scope",
|
|
prepare: getTokens,
|
|
scope: []string{oidc.ScopeProfile, oidc.ScopeOpenID, oidc.ScopeEmail, oidc.ScopeOfflineAccess, oidc_api.ScopeProjectRolePrefix + role},
|
|
assertions: []func(*testing.T, *oidc.UserInfo){
|
|
assertUserinfo,
|
|
func(t *testing.T, ui *oidc.UserInfo) {
|
|
assertProjectRoleClaims(t, projectID, ui.Claims, role)
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "PAT",
|
|
prepare: func(t *testing.T, clientID string, scope []string) *oidc.Tokens[*oidc.IDTokenClaims] {
|
|
user := Tester.Users.Get(integration.FirstInstanceUsersKey, integration.OrgOwner)
|
|
return &oidc.Tokens[*oidc.IDTokenClaims]{
|
|
Token: &oauth2.Token{
|
|
AccessToken: user.Token,
|
|
TokenType: oidc.BearerToken,
|
|
},
|
|
IDTokenClaims: &oidc.IDTokenClaims{
|
|
TokenClaims: oidc.TokenClaims{
|
|
Subject: user.ID,
|
|
},
|
|
},
|
|
}
|
|
},
|
|
assertions: []func(*testing.T, *oidc.UserInfo){
|
|
func(t *testing.T, ui *oidc.UserInfo) {
|
|
user := Tester.Users.Get(integration.FirstInstanceUsersKey, integration.OrgOwner)
|
|
assert.Equal(t, user.ID, ui.Subject)
|
|
assert.Equal(t, user.PreferredLoginName, ui.PreferredUsername)
|
|
assert.Equal(t, user.Machine.Name, ui.Name)
|
|
assert.Equal(t, user.ResourceOwner, ui.Claims[oidc_api.ClaimResourceOwnerID])
|
|
assert.NotEmpty(t, ui.Claims[oidc_api.ClaimResourceOwnerName])
|
|
assert.NotEmpty(t, ui.Claims[oidc_api.ClaimResourceOwnerPrimaryDomain])
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tokens := tt.prepare(t, clientID, tt.scope)
|
|
provider, err := Tester.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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func getTokens(t *testing.T, clientID string, scope []string) *oidc.Tokens[*oidc.IDTokenClaims] {
|
|
authRequestID := createAuthRequest(t, clientID, redirectURI, scope...)
|
|
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
|
|
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
|
|
AuthRequestId: authRequestID,
|
|
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
|
Session: &oidc_pb.Session{
|
|
SessionId: sessionID,
|
|
SessionToken: sessionToken,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// code exchange
|
|
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
|
|
tokens, err := exchangeTokens(t, clientID, code, redirectURI)
|
|
require.NoError(t, err)
|
|
assertTokens(t, tokens, true)
|
|
assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime)
|
|
|
|
return tokens
|
|
}
|
|
|
|
func assertUserinfo(t *testing.T, userinfo *oidc.UserInfo) {
|
|
t.Helper()
|
|
assert.Equal(t, User.GetUserId(), userinfo.Subject)
|
|
assert.Equal(t, "Mickey", userinfo.GivenName)
|
|
assert.Equal(t, "Mouse", userinfo.FamilyName)
|
|
assert.Equal(t, "Mickey Mouse", userinfo.Name)
|
|
assert.NotEmpty(t, userinfo.PreferredUsername)
|
|
assert.Equal(t, userinfo.PreferredUsername, userinfo.Email)
|
|
assert.False(t, bool(userinfo.EmailVerified))
|
|
assertOIDCTime(t, userinfo.UpdatedAt, User.GetDetails().GetChangeDate().AsTime())
|
|
}
|
|
|
|
func assertNoReservedScopes(t *testing.T, claims map[string]any) {
|
|
t.Helper()
|
|
t.Log(claims)
|
|
for claim := range claims {
|
|
assert.Falsef(t, strings.HasPrefix(claim, oidc_api.ClaimPrefix), "claim %s has prefix %s", claim, oidc_api.ClaimPrefix)
|
|
}
|
|
}
|
|
|
|
func assertProjectRoleClaims(t *testing.T, projectID string, claims map[string]any, roles ...string) {
|
|
t.Helper()
|
|
projectIDRoleClaim := fmt.Sprintf(oidc_api.ClaimProjectRolesFormat, projectID)
|
|
for _, claim := range []string{oidc_api.ClaimProjectRoles, projectIDRoleClaim} {
|
|
roleMap, ok := claims[claim].(map[string]any)
|
|
require.Truef(t, ok, "claim %s not found or wrong type %T", claim, claims[claim])
|
|
for _, roleKey := range roles {
|
|
role, ok := roleMap[roleKey].(map[string]any)
|
|
require.Truef(t, ok, "role %s not found or wrong type %T", roleKey, roleMap[roleKey])
|
|
assert.Equal(t, role[Tester.Organisation.ID], Tester.Organisation.Domain, "org domain in role")
|
|
}
|
|
}
|
|
}
|