mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
# Which Problems Are Solved Expose impersonating actor to `preAccessToken` and `preUserinfo` actions. # How the Problems Are Solved Provide the `actor` information already present in the OIDC session model of JWT token to the relevant actions. Both goja-based actions "v1" and webhook based execution targets carry the actor information now. Actor remains null in case of non-impersonated tokens. Actor may contain nested actors to display a delegation chain of impersonators. # Additional Changes - dba6261c8ed0dfd00ffde6193910b1405b17432f: refactor `userinfoFlows` to reduce complexity, add test coverage and solve a couple of potential bugs. - `getClientId` in actions was previously undocumented. Added to documentation. - A skill that reproduces manual testing using webhook.site or a local sink. # Additional Context - Closes https://github.com/zitadel/zitadel/issues/12097 --------- Co-authored-by: Claude Sonnet 5 <[email protected]> Co-authored-by: Copilot Autofix powered by AI <[email protected]> Co-authored-by: Wim Van Laer <[email protected]>
743 lines
28 KiB
Go
743 lines
28 KiB
Go
//go:build integration
|
|
|
|
package oidc_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/zitadel/oidc/v3/pkg/client"
|
|
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
|
"github.com/zitadel/oidc/v3/pkg/client/rs"
|
|
"github.com/zitadel/oidc/v3/pkg/client/tokenexchange"
|
|
"github.com/zitadel/oidc/v3/pkg/crypto"
|
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
|
|
oidc_api "github.com/zitadel/zitadel/internal/api/oidc"
|
|
"github.com/zitadel/zitadel/internal/domain"
|
|
"github.com/zitadel/zitadel/internal/integration"
|
|
"github.com/zitadel/zitadel/pkg/grpc/admin"
|
|
"github.com/zitadel/zitadel/pkg/grpc/app"
|
|
"github.com/zitadel/zitadel/pkg/grpc/management"
|
|
)
|
|
|
|
func setImpersonationPolicy(t *testing.T, instance *integration.Instance, value bool) {
|
|
instance.SetImpersonationPolicy(CTX, t, value)
|
|
}
|
|
|
|
func createMachineUserPATWithMembership(ctx context.Context, t *testing.T, instance *integration.Instance, roles ...string) (userID, pat string) {
|
|
userID, pat, err := instance.CreateMachineUserPATWithMembership(ctx, roles...)
|
|
require.NoError(t, err)
|
|
return userID, pat
|
|
}
|
|
|
|
func accessTokenVerifier(ctx context.Context, server rs.ResourceServer, subject, actorSubject string) func(t *testing.T, token string) {
|
|
return func(t *testing.T, token string) {
|
|
resp, err := rs.Introspect[*oidc.IntrospectionResponse](ctx, server, token)
|
|
require.NoError(t, err)
|
|
assert.True(t, resp.Active)
|
|
if subject != "" {
|
|
assert.Equal(t, subject, resp.Subject)
|
|
}
|
|
if actorSubject != "" {
|
|
require.NotNil(t, resp.Actor)
|
|
assert.Equal(t, actorSubject, resp.Actor.Subject)
|
|
}
|
|
}
|
|
}
|
|
|
|
func idTokenVerifier(ctx context.Context, provider rp.RelyingParty, subject, actorSubject string) func(t *testing.T, token string) {
|
|
return func(t *testing.T, token string) {
|
|
verifier := provider.IDTokenVerifier()
|
|
resp, err := rp.VerifyIDToken[*oidc.IDTokenClaims](ctx, token, verifier)
|
|
require.NoError(t, err)
|
|
if subject != "" {
|
|
assert.Equal(t, subject, resp.Subject)
|
|
}
|
|
if actorSubject != "" {
|
|
require.NotNil(t, resp.Actor)
|
|
assert.Equal(t, actorSubject, resp.Actor.Subject)
|
|
}
|
|
}
|
|
}
|
|
|
|
func refreshTokenVerifier(ctx context.Context, provider rp.RelyingParty, subject, actorSubject string) func(t *testing.T, token string) {
|
|
return func(t *testing.T, token string) {
|
|
clientAssertion, err := client.SignedJWTProfileAssertion(provider.OAuthConfig().ClientID, []string{provider.Issuer()}, time.Hour, provider.Signer())
|
|
require.NoError(t, err)
|
|
tokens, err := rp.RefreshTokens[*oidc.IDTokenClaims](ctx, provider, token, clientAssertion, oidc.ClientAssertionTypeJWTAssertion)
|
|
require.NoError(t, err)
|
|
|
|
if subject != "" {
|
|
assert.Equal(t, subject, tokens.IDTokenClaims.Subject)
|
|
}
|
|
if actorSubject != "" {
|
|
require.NotNil(t, tokens.IDTokenClaims.Actor)
|
|
assert.Equal(t, actorSubject, tokens.IDTokenClaims.Actor.Subject)
|
|
}
|
|
assert.NotEmpty(t, tokens.RefreshToken)
|
|
}
|
|
}
|
|
|
|
func TestServer_TokenExchange(t *testing.T) {
|
|
instance := integration.NewInstance(CTX)
|
|
ctx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
|
|
userResp := instance.CreateHumanUser(ctx)
|
|
|
|
client, keyData, err := instance.CreateOIDCTokenExchangeClient(ctx, t)
|
|
require.NoError(t, err)
|
|
signer, err := rp.SignerFromKeyFile(keyData)()
|
|
require.NoError(t, err)
|
|
exchanger, err := tokenexchange.NewTokenExchangerJWTProfile(ctx, instance.OIDCIssuer(), client.GetClientId(), signer)
|
|
require.NoError(t, err)
|
|
|
|
_, orgImpersonatorPAT := createMachineUserPATWithMembership(ctx, t, instance, "ORG_ADMIN_IMPERSONATOR")
|
|
serviceUserID, noPermPAT := createMachineUserPATWithMembership(ctx, t, instance)
|
|
|
|
teResp, err := tokenexchange.ExchangeToken(ctx, exchanger, noPermPAT, oidc.AccessTokenType, "", "", nil, nil, nil, oidc.AccessTokenType)
|
|
require.NoError(t, err)
|
|
|
|
patScopes := oidc.SpaceDelimitedArray{"openid", "profile", "urn:zitadel:iam:user:metadata", "urn:zitadel:iam:user:resourceowner"}
|
|
|
|
relyingParty, err := rp.NewRelyingPartyOIDC(ctx, instance.OIDCIssuer(), client.GetClientId(), "", "", []string{"openid"}, rp.WithJWTProfile(rp.SignerFromKeyFile(keyData)))
|
|
require.NoError(t, err)
|
|
resourceServer, err := instance.CreateResourceServerJWTProfile(ctx, keyData)
|
|
require.NoError(t, err)
|
|
|
|
type args struct {
|
|
SubjectToken string
|
|
SubjectTokenType oidc.TokenType
|
|
ActorToken string
|
|
ActorTokenType oidc.TokenType
|
|
Resource []string
|
|
Audience []string
|
|
Scopes []string
|
|
RequestedTokenType oidc.TokenType
|
|
}
|
|
type result struct {
|
|
issuedTokenType oidc.TokenType
|
|
tokenType string
|
|
expiresIn uint64
|
|
scopes oidc.SpaceDelimitedArray
|
|
verifyAccessToken func(t *testing.T, token string)
|
|
verifyRefreshToken func(t *testing.T, token string)
|
|
verifyIDToken func(t *testing.T, token string)
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want result
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "unsupported resource parameter",
|
|
args: args{
|
|
SubjectToken: noPermPAT,
|
|
SubjectTokenType: oidc.AccessTokenType,
|
|
Resource: []string{"https://example.com"},
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid subject token",
|
|
args: args{
|
|
SubjectToken: "foo",
|
|
SubjectTokenType: oidc.AccessTokenType,
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "EXCHANGE: access token to default",
|
|
args: args{
|
|
SubjectToken: noPermPAT,
|
|
SubjectTokenType: oidc.AccessTokenType,
|
|
},
|
|
want: result{
|
|
issuedTokenType: oidc.AccessTokenType,
|
|
tokenType: oidc.BearerToken,
|
|
expiresIn: 43100,
|
|
scopes: patScopes,
|
|
verifyAccessToken: accessTokenVerifier(ctx, resourceServer, serviceUserID, ""),
|
|
verifyIDToken: idTokenVerifier(ctx, relyingParty, serviceUserID, ""),
|
|
},
|
|
},
|
|
{
|
|
name: "EXCHANGE: access token to access token",
|
|
args: args{
|
|
SubjectToken: noPermPAT,
|
|
SubjectTokenType: oidc.AccessTokenType,
|
|
RequestedTokenType: oidc.AccessTokenType,
|
|
},
|
|
want: result{
|
|
issuedTokenType: oidc.AccessTokenType,
|
|
tokenType: oidc.BearerToken,
|
|
expiresIn: 43100,
|
|
scopes: patScopes,
|
|
verifyAccessToken: accessTokenVerifier(ctx, resourceServer, serviceUserID, ""),
|
|
verifyIDToken: idTokenVerifier(ctx, relyingParty, serviceUserID, ""),
|
|
},
|
|
},
|
|
{
|
|
name: "EXCHANGE: access token to JWT",
|
|
args: args{
|
|
SubjectToken: noPermPAT,
|
|
SubjectTokenType: oidc.AccessTokenType,
|
|
RequestedTokenType: oidc.JWTTokenType,
|
|
},
|
|
want: result{
|
|
issuedTokenType: oidc.JWTTokenType,
|
|
tokenType: oidc.BearerToken,
|
|
expiresIn: 43100,
|
|
scopes: patScopes,
|
|
verifyAccessToken: accessTokenVerifier(ctx, resourceServer, serviceUserID, ""),
|
|
verifyIDToken: idTokenVerifier(ctx, relyingParty, serviceUserID, ""),
|
|
},
|
|
},
|
|
{
|
|
name: "EXCHANGE: access token to ID Token",
|
|
args: args{
|
|
SubjectToken: noPermPAT,
|
|
SubjectTokenType: oidc.AccessTokenType,
|
|
RequestedTokenType: oidc.IDTokenType,
|
|
},
|
|
want: result{
|
|
issuedTokenType: oidc.IDTokenType,
|
|
tokenType: "N_A",
|
|
expiresIn: 43100,
|
|
scopes: patScopes,
|
|
verifyAccessToken: idTokenVerifier(ctx, relyingParty, serviceUserID, ""),
|
|
verifyIDToken: func(t *testing.T, token string) {
|
|
assert.Empty(t, token)
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "EXCHANGE: refresh token not allowed",
|
|
args: args{
|
|
SubjectToken: teResp.RefreshToken,
|
|
SubjectTokenType: oidc.RefreshTokenType,
|
|
RequestedTokenType: oidc.IDTokenType,
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "EXCHANGE: alternate scope for refresh token is rejected when not subset of subject scopes",
|
|
args: args{
|
|
SubjectToken: noPermPAT,
|
|
SubjectTokenType: oidc.AccessTokenType,
|
|
RequestedTokenType: oidc.AccessTokenType,
|
|
Scopes: []string{oidc.ScopeOpenID, oidc.ScopeOfflineAccess, "profile"},
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "EXCHANGE: access token, requested token type not supported error",
|
|
args: args{
|
|
SubjectToken: noPermPAT,
|
|
SubjectTokenType: oidc.AccessTokenType,
|
|
RequestedTokenType: oidc.RefreshTokenType,
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "EXCHANGE: access token, invalid audience",
|
|
args: args{
|
|
SubjectToken: noPermPAT,
|
|
SubjectTokenType: oidc.AccessTokenType,
|
|
RequestedTokenType: oidc.AccessTokenType,
|
|
Audience: []string{"foo", "bar"},
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "IMPERSONATION: subject: userID, actor: access token, policy disabled error",
|
|
args: args{
|
|
SubjectToken: userResp.GetUserId(),
|
|
SubjectTokenType: oidc_api.UserIDTokenType,
|
|
RequestedTokenType: oidc.AccessTokenType,
|
|
ActorToken: orgImpersonatorPAT,
|
|
ActorTokenType: oidc.AccessTokenType,
|
|
},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := tokenexchange.ExchangeToken(ctx, exchanger, tt.args.SubjectToken, tt.args.SubjectTokenType, tt.args.ActorToken, tt.args.ActorTokenType, tt.args.Resource, tt.args.Audience, tt.args.Scopes, tt.args.RequestedTokenType)
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.want.issuedTokenType, got.IssuedTokenType)
|
|
assert.Equal(t, tt.want.tokenType, got.TokenType)
|
|
assert.Greater(t, got.ExpiresIn, tt.want.expiresIn)
|
|
assert.Equal(t, tt.want.scopes, got.Scopes)
|
|
if tt.want.verifyAccessToken != nil {
|
|
tt.want.verifyAccessToken(t, got.AccessToken)
|
|
}
|
|
if tt.want.verifyRefreshToken != nil {
|
|
tt.want.verifyRefreshToken(t, got.RefreshToken)
|
|
}
|
|
if tt.want.verifyIDToken != nil {
|
|
tt.want.verifyIDToken(t, got.IDToken)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServer_TokenExchange_GHSA_vrh8_c9cm_wh8v(t *testing.T) {
|
|
instance := integration.NewInstance(CTX)
|
|
ctx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
|
|
|
|
t.Run("SECURITY: client should not exchange access tokens of other clients", func(t *testing.T) {
|
|
// Create two separate token-exchange clients (different projects).
|
|
clientA, keyDataA, err := instance.CreateOIDCTokenExchangeClient(ctx, t)
|
|
require.NoError(t, err)
|
|
signerA, err := rp.SignerFromKeyFile(keyDataA)()
|
|
require.NoError(t, err)
|
|
exchangerA, err := tokenexchange.NewTokenExchangerJWTProfile(ctx, instance.OIDCIssuer(), clientA.GetClientId(), signerA)
|
|
require.NoError(t, err)
|
|
|
|
clientB, keyDataB, err := instance.CreateOIDCTokenExchangeClient(ctx, t)
|
|
require.NoError(t, err)
|
|
signerB, err := rp.SignerFromKeyFile(keyDataB)()
|
|
require.NoError(t, err)
|
|
exchangerB, err := tokenexchange.NewTokenExchangerJWTProfile(ctx, instance.OIDCIssuer(), clientB.GetClientId(), signerB)
|
|
require.NoError(t, err)
|
|
|
|
// Mint a valid access token for clientA to use as subject_token.
|
|
_, servicePAT := createMachineUserPATWithMembership(ctx, t, instance)
|
|
source, err := tokenexchange.ExchangeToken(ctx, exchangerA, servicePAT, oidc.AccessTokenType, "", "", nil, nil, []string{oidc.ScopeOpenID}, oidc.AccessTokenType)
|
|
require.NoError(t, err)
|
|
|
|
// RFC 8693 requires that the exchanged token belongs to the requesting client.
|
|
_, err = tokenexchange.ExchangeToken(ctx, exchangerB, source.AccessToken, oidc.AccessTokenType, "", "", nil, nil, nil, oidc.AccessTokenType)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "subject_token invalid")
|
|
})
|
|
|
|
t.Run("SECURITY: requested scopes must be subset of subject token scopes", func(t *testing.T) {
|
|
clientA, keyDataA, err := instance.CreateOIDCTokenExchangeClient(ctx, t)
|
|
require.NoError(t, err)
|
|
signerA, err := rp.SignerFromKeyFile(keyDataA)()
|
|
require.NoError(t, err)
|
|
exchangerA, err := tokenexchange.NewTokenExchangerJWTProfile(ctx, instance.OIDCIssuer(), clientA.GetClientId(), signerA)
|
|
require.NoError(t, err)
|
|
|
|
_, servicePAT := createMachineUserPATWithMembership(ctx, t, instance)
|
|
limited, err := tokenexchange.ExchangeToken(ctx, exchangerA, servicePAT, oidc.AccessTokenType, "", "", nil, nil, nil, oidc.AccessTokenType)
|
|
require.NoError(t, err)
|
|
assert.NotContains(t, limited.Scopes, oidc.ScopeOfflineAccess)
|
|
|
|
// Asking for offline_access escalates beyond the subject token's scope set and must fail.
|
|
_, err = tokenexchange.ExchangeToken(ctx, exchangerA, limited.AccessToken, oidc.AccessTokenType, "", "", nil, nil, []string{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}, oidc.AccessTokenType)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "invalid_scope")
|
|
})
|
|
}
|
|
|
|
func TestServer_TokenExchangeImpersonation(t *testing.T) {
|
|
instance := integration.NewInstance(CTX)
|
|
ctx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
|
|
userResp := instance.CreateHumanUser(ctx)
|
|
|
|
// exchange some tokens for later use
|
|
setImpersonationPolicy(t, instance, true)
|
|
|
|
client, keyData, err := instance.CreateOIDCTokenExchangeClient(ctx, t)
|
|
require.NoError(t, err)
|
|
signer, err := rp.SignerFromKeyFile(keyData)()
|
|
require.NoError(t, err)
|
|
exchanger, err := tokenexchange.NewTokenExchangerJWTProfile(ctx, instance.OIDCIssuer(), client.GetClientId(), signer)
|
|
require.NoError(t, err)
|
|
|
|
iamUserID, iamImpersonatorPAT := createMachineUserPATWithMembership(ctx, t, instance, "IAM_ADMIN_IMPERSONATOR")
|
|
orgUserID, orgImpersonatorPAT := createMachineUserPATWithMembership(ctx, t, instance, "ORG_ADMIN_IMPERSONATOR")
|
|
serviceUserID, noPermPAT := createMachineUserPATWithMembership(ctx, t, instance)
|
|
|
|
teResp, err := tokenexchange.ExchangeToken(ctx, exchanger, noPermPAT, oidc.AccessTokenType, "", "", nil, nil, nil, oidc.AccessTokenType)
|
|
require.NoError(t, err)
|
|
|
|
patScopes := oidc.SpaceDelimitedArray{"openid", "profile", "urn:zitadel:iam:user:metadata", "urn:zitadel:iam:user:resourceowner"}
|
|
|
|
relyingParty, err := rp.NewRelyingPartyOIDC(ctx, instance.OIDCIssuer(), client.GetClientId(), "", "", []string{"openid"}, rp.WithJWTProfile(rp.SignerFromKeyFile(keyData)))
|
|
require.NoError(t, err)
|
|
resourceServer, err := instance.CreateResourceServerJWTProfile(ctx, keyData)
|
|
require.NoError(t, err)
|
|
|
|
type args struct {
|
|
SubjectToken string
|
|
SubjectTokenType oidc.TokenType
|
|
ActorToken string
|
|
ActorTokenType oidc.TokenType
|
|
Resource []string
|
|
Audience []string
|
|
Scopes []string
|
|
RequestedTokenType oidc.TokenType
|
|
}
|
|
type result struct {
|
|
issuedTokenType oidc.TokenType
|
|
tokenType string
|
|
expiresIn uint64
|
|
scopes oidc.SpaceDelimitedArray
|
|
verifyAccessToken func(t *testing.T, token string)
|
|
verifyRefreshToken func(t *testing.T, token string)
|
|
verifyIDToken func(t *testing.T, token string)
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want result
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "IMPERSONATION: subject: userID, actor: access token, membership not found error",
|
|
args: args{
|
|
SubjectToken: userResp.GetUserId(),
|
|
SubjectTokenType: oidc_api.UserIDTokenType,
|
|
RequestedTokenType: oidc.AccessTokenType,
|
|
ActorToken: noPermPAT,
|
|
ActorTokenType: oidc.AccessTokenType,
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "IMPERSONATION: subject: userID, actor: access token, requested type: JWT, membership not found error",
|
|
args: args{
|
|
SubjectToken: userResp.GetUserId(),
|
|
SubjectTokenType: oidc_api.UserIDTokenType,
|
|
RequestedTokenType: oidc.JWTTokenType,
|
|
ActorToken: noPermPAT,
|
|
ActorTokenType: oidc.AccessTokenType,
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "Instance IMPERSONATION: subject: userID, actor: access token, success",
|
|
args: args{
|
|
SubjectToken: userResp.GetUserId(),
|
|
SubjectTokenType: oidc_api.UserIDTokenType,
|
|
RequestedTokenType: oidc.AccessTokenType,
|
|
ActorToken: iamImpersonatorPAT,
|
|
ActorTokenType: oidc.AccessTokenType,
|
|
},
|
|
want: result{
|
|
issuedTokenType: oidc.AccessTokenType,
|
|
tokenType: oidc.BearerToken,
|
|
expiresIn: 43100,
|
|
scopes: patScopes,
|
|
verifyAccessToken: accessTokenVerifier(ctx, resourceServer, userResp.GetUserId(), iamUserID),
|
|
verifyIDToken: idTokenVerifier(ctx, relyingParty, userResp.GetUserId(), iamUserID),
|
|
},
|
|
},
|
|
{
|
|
name: "ORG IMPERSONATION: subject: userID, actor: access token, success",
|
|
args: args{
|
|
SubjectToken: userResp.GetUserId(),
|
|
SubjectTokenType: oidc_api.UserIDTokenType,
|
|
RequestedTokenType: oidc.AccessTokenType,
|
|
ActorToken: orgImpersonatorPAT,
|
|
ActorTokenType: oidc.AccessTokenType,
|
|
},
|
|
want: result{
|
|
issuedTokenType: oidc.AccessTokenType,
|
|
tokenType: oidc.BearerToken,
|
|
expiresIn: 43100,
|
|
scopes: patScopes,
|
|
verifyAccessToken: accessTokenVerifier(ctx, resourceServer, userResp.GetUserId(), orgUserID),
|
|
verifyIDToken: idTokenVerifier(ctx, relyingParty, userResp.GetUserId(), orgUserID),
|
|
},
|
|
},
|
|
{
|
|
name: "IMPERSONATION: subject: userID, actor: access token, reduced scopes, success",
|
|
args: args{
|
|
SubjectToken: userResp.GetUserId(),
|
|
SubjectTokenType: oidc_api.UserIDTokenType,
|
|
RequestedTokenType: oidc.AccessTokenType,
|
|
ActorToken: orgImpersonatorPAT,
|
|
ActorTokenType: oidc.AccessTokenType,
|
|
Scopes: patScopes[:2],
|
|
},
|
|
want: result{
|
|
issuedTokenType: oidc.AccessTokenType,
|
|
tokenType: oidc.BearerToken,
|
|
expiresIn: 43100,
|
|
scopes: patScopes[:2],
|
|
verifyAccessToken: accessTokenVerifier(ctx, resourceServer, userResp.GetUserId(), orgUserID),
|
|
verifyIDToken: idTokenVerifier(ctx, relyingParty, userResp.GetUserId(), orgUserID),
|
|
},
|
|
},
|
|
{
|
|
name: "IMPERSONATION: subject: userID, actor: access token, email scope not on actor, success",
|
|
args: args{
|
|
SubjectToken: userResp.GetUserId(),
|
|
SubjectTokenType: oidc_api.UserIDTokenType,
|
|
RequestedTokenType: oidc.AccessTokenType,
|
|
ActorToken: orgImpersonatorPAT,
|
|
ActorTokenType: oidc.AccessTokenType,
|
|
Scopes: []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail},
|
|
},
|
|
want: result{
|
|
issuedTokenType: oidc.AccessTokenType,
|
|
tokenType: oidc.BearerToken,
|
|
expiresIn: 43100,
|
|
scopes: oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail},
|
|
verifyAccessToken: accessTokenVerifier(ctx, resourceServer, userResp.GetUserId(), orgUserID),
|
|
verifyIDToken: idTokenVerifier(ctx, relyingParty, userResp.GetUserId(), orgUserID),
|
|
},
|
|
},
|
|
{
|
|
name: "IMPERSONATION: subject: userID, actor: access token, openid only, success",
|
|
args: args{
|
|
SubjectToken: userResp.GetUserId(),
|
|
SubjectTokenType: oidc_api.UserIDTokenType,
|
|
RequestedTokenType: oidc.AccessTokenType,
|
|
ActorToken: orgImpersonatorPAT,
|
|
ActorTokenType: oidc.AccessTokenType,
|
|
Scopes: []string{oidc.ScopeOpenID},
|
|
},
|
|
want: result{
|
|
issuedTokenType: oidc.AccessTokenType,
|
|
tokenType: oidc.BearerToken,
|
|
expiresIn: 43100,
|
|
scopes: oidc.SpaceDelimitedArray{oidc.ScopeOpenID},
|
|
verifyAccessToken: accessTokenVerifier(ctx, resourceServer, userResp.GetUserId(), orgUserID),
|
|
},
|
|
},
|
|
{
|
|
name: "IMPERSONATION: subject: userID, actor: access token, offline_access not on actor, rejected",
|
|
args: args{
|
|
SubjectToken: userResp.GetUserId(),
|
|
SubjectTokenType: oidc_api.UserIDTokenType,
|
|
RequestedTokenType: oidc.AccessTokenType,
|
|
ActorToken: orgImpersonatorPAT,
|
|
ActorTokenType: oidc.AccessTokenType,
|
|
Scopes: []string{oidc.ScopeOpenID, oidc.ScopeOfflineAccess},
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "IMPERSONATION: subject: ID token, actor: access token, email scope not on actor, success",
|
|
args: args{
|
|
SubjectToken: teResp.IDToken,
|
|
SubjectTokenType: oidc.IDTokenType,
|
|
RequestedTokenType: oidc.AccessTokenType,
|
|
ActorToken: orgImpersonatorPAT,
|
|
ActorTokenType: oidc.AccessTokenType,
|
|
Scopes: []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail},
|
|
},
|
|
want: result{
|
|
issuedTokenType: oidc.AccessTokenType,
|
|
tokenType: oidc.BearerToken,
|
|
expiresIn: 43100,
|
|
scopes: oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail},
|
|
verifyAccessToken: accessTokenVerifier(ctx, resourceServer, serviceUserID, orgUserID),
|
|
verifyIDToken: idTokenVerifier(ctx, relyingParty, serviceUserID, orgUserID),
|
|
},
|
|
},
|
|
{
|
|
name: "IMPERSONATION: subject: access token, actor: access token, email not on input tokens, rejected",
|
|
args: args{
|
|
SubjectToken: teResp.AccessToken,
|
|
SubjectTokenType: oidc.AccessTokenType,
|
|
RequestedTokenType: oidc.AccessTokenType,
|
|
ActorToken: orgImpersonatorPAT,
|
|
ActorTokenType: oidc.AccessTokenType,
|
|
Scopes: []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail},
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "ORG IMPERSONATION: subject: access token, actor: access token, success",
|
|
args: args{
|
|
SubjectToken: teResp.AccessToken,
|
|
SubjectTokenType: oidc.AccessTokenType,
|
|
RequestedTokenType: oidc.AccessTokenType,
|
|
ActorToken: orgImpersonatorPAT,
|
|
ActorTokenType: oidc.AccessTokenType,
|
|
},
|
|
want: result{
|
|
issuedTokenType: oidc.AccessTokenType,
|
|
tokenType: oidc.BearerToken,
|
|
expiresIn: 43100,
|
|
scopes: patScopes,
|
|
verifyAccessToken: accessTokenVerifier(ctx, resourceServer, serviceUserID, orgUserID),
|
|
verifyIDToken: idTokenVerifier(ctx, relyingParty, serviceUserID, orgUserID),
|
|
},
|
|
},
|
|
{
|
|
name: "ORG IMPERSONATION: subject: ID token, actor: access token, success",
|
|
args: args{
|
|
SubjectToken: teResp.IDToken,
|
|
SubjectTokenType: oidc.IDTokenType,
|
|
RequestedTokenType: oidc.AccessTokenType,
|
|
ActorToken: orgImpersonatorPAT,
|
|
ActorTokenType: oidc.AccessTokenType,
|
|
},
|
|
want: result{
|
|
issuedTokenType: oidc.AccessTokenType,
|
|
tokenType: oidc.BearerToken,
|
|
expiresIn: 43100,
|
|
scopes: patScopes,
|
|
verifyAccessToken: accessTokenVerifier(ctx, resourceServer, serviceUserID, orgUserID),
|
|
verifyIDToken: idTokenVerifier(ctx, relyingParty, serviceUserID, orgUserID),
|
|
},
|
|
},
|
|
{
|
|
name: "ORG IMPERSONATION: subject: JWT, actor: access token, success",
|
|
args: args{
|
|
SubjectToken: func() string {
|
|
token, err := crypto.Sign(&oidc.JWTTokenRequest{
|
|
Issuer: client.GetClientId(),
|
|
Subject: userResp.GetUserId(),
|
|
Audience: oidc.Audience{instance.OIDCIssuer()},
|
|
ExpiresAt: oidc.FromTime(time.Now().Add(time.Hour)),
|
|
IssuedAt: oidc.FromTime(time.Now().Add(-time.Second)),
|
|
}, signer)
|
|
require.NoError(t, err)
|
|
return token
|
|
}(),
|
|
SubjectTokenType: oidc.JWTTokenType,
|
|
RequestedTokenType: oidc.AccessTokenType,
|
|
ActorToken: orgImpersonatorPAT,
|
|
ActorTokenType: oidc.AccessTokenType,
|
|
},
|
|
want: result{
|
|
issuedTokenType: oidc.AccessTokenType,
|
|
tokenType: oidc.BearerToken,
|
|
expiresIn: 43100,
|
|
scopes: patScopes,
|
|
verifyAccessToken: accessTokenVerifier(ctx, resourceServer, userResp.GetUserId(), orgUserID),
|
|
verifyIDToken: idTokenVerifier(ctx, relyingParty, userResp.GetUserId(), orgUserID),
|
|
},
|
|
},
|
|
{
|
|
name: "ORG IMPERSONATION: subject: access token, actor: access token, reduced scopes success",
|
|
args: args{
|
|
SubjectToken: teResp.AccessToken,
|
|
SubjectTokenType: oidc.AccessTokenType,
|
|
RequestedTokenType: oidc.AccessTokenType,
|
|
ActorToken: orgImpersonatorPAT,
|
|
ActorTokenType: oidc.AccessTokenType,
|
|
Scopes: []string{oidc.ScopeOpenID},
|
|
},
|
|
want: result{
|
|
issuedTokenType: oidc.AccessTokenType,
|
|
tokenType: oidc.BearerToken,
|
|
expiresIn: 43100,
|
|
scopes: oidc.SpaceDelimitedArray{oidc.ScopeOpenID},
|
|
verifyAccessToken: accessTokenVerifier(ctx, resourceServer, serviceUserID, orgUserID),
|
|
verifyIDToken: idTokenVerifier(ctx, relyingParty, serviceUserID, orgUserID),
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := tokenexchange.ExchangeToken(ctx, exchanger, tt.args.SubjectToken, tt.args.SubjectTokenType, tt.args.ActorToken, tt.args.ActorTokenType, tt.args.Resource, tt.args.Audience, tt.args.Scopes, tt.args.RequestedTokenType)
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.want.issuedTokenType, got.IssuedTokenType)
|
|
assert.Equal(t, tt.want.tokenType, got.TokenType)
|
|
assert.Greater(t, got.ExpiresIn, tt.want.expiresIn)
|
|
assert.Equal(t, tt.want.scopes, got.Scopes)
|
|
if tt.want.verifyAccessToken != nil {
|
|
tt.want.verifyAccessToken(t, got.AccessToken)
|
|
}
|
|
if tt.want.verifyRefreshToken != nil {
|
|
tt.want.verifyRefreshToken(t, got.RefreshToken)
|
|
}
|
|
if tt.want.verifyIDToken != nil {
|
|
tt.want.verifyIDToken(t, got.IDToken)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServer_TokenExchangeImpersonationClientCredentialsActor(t *testing.T) {
|
|
instance := integration.NewInstance(CTX)
|
|
ctx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
|
|
userResp := instance.CreateHumanUser(ctx)
|
|
|
|
setImpersonationPolicy(t, instance, true)
|
|
|
|
project := instance.CreateProject(ctx, t, "", integration.ProjectName(), false, false)
|
|
client, keyData, err := instance.CreateOIDCWebClientJWT(ctx, "", "", project.GetId(),
|
|
app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE,
|
|
app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
|
|
app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN,
|
|
)
|
|
require.NoError(t, err)
|
|
signer, err := rp.SignerFromKeyFile(keyData)()
|
|
require.NoError(t, err)
|
|
exchanger, err := tokenexchange.NewTokenExchangerJWTProfile(ctx, instance.OIDCIssuer(), client.GetClientId(), signer)
|
|
require.NoError(t, err)
|
|
resourceServer, err := instance.CreateResourceServerJWTProfile(ctx, keyData)
|
|
require.NoError(t, err)
|
|
|
|
impersonatorUser := instance.CreateMachineUser(ctx)
|
|
_, err = instance.Client.Mgmt.AddOrgMember(ctx, &management.AddOrgMemberRequest{
|
|
UserId: impersonatorUser.GetUserId(),
|
|
Roles: []string{"ORG_ADMIN_IMPERSONATOR"},
|
|
})
|
|
require.NoError(t, err)
|
|
secret, err := instance.Client.Mgmt.GenerateMachineSecret(ctx, &management.GenerateMachineSecretRequest{
|
|
UserId: impersonatorUser.GetUserId(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
audScope := domain.ProjectIDScope + project.GetId() + domain.AudSuffix
|
|
actorScopes := []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc_api.ScopeProjectsRoles, audScope}
|
|
credentialsProvider, err := rp.NewRelyingPartyOIDC(ctx, instance.OIDCIssuer(), secret.GetClientId(), secret.GetClientSecret(), "", actorScopes)
|
|
require.NoError(t, err)
|
|
actorTokens, err := rp.ClientCredentials(ctx, credentialsProvider, nil)
|
|
require.NoError(t, err)
|
|
|
|
step2Scopes := []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc_api.ScopeProjectsRoles, audScope}
|
|
got, err := tokenexchange.ExchangeToken(ctx, exchanger, userResp.GetUserId(), oidc_api.UserIDTokenType, actorTokens.AccessToken, oidc.AccessTokenType, nil, nil, step2Scopes, oidc.AccessTokenType)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, oidc.SpaceDelimitedArray(step2Scopes), got.Scopes)
|
|
accessTokenVerifier(ctx, resourceServer, userResp.GetUserId(), impersonatorUser.GetUserId())(t, got.AccessToken)
|
|
}
|
|
|
|
// This test tries to call the zitadel API with an impersonated token,
|
|
// which should fail.
|
|
func TestImpersonation_API_Call(t *testing.T) {
|
|
instance := integration.NewInstance(CTX)
|
|
ctx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
|
|
|
|
client, keyData, err := instance.CreateOIDCTokenExchangeClient(ctx, t)
|
|
require.NoError(t, err)
|
|
signer, err := rp.SignerFromKeyFile(keyData)()
|
|
require.NoError(t, err)
|
|
exchanger, err := tokenexchange.NewTokenExchangerJWTProfile(ctx, instance.OIDCIssuer(), client.GetClientId(), signer)
|
|
require.NoError(t, err)
|
|
resourceServer, err := instance.CreateResourceServerJWTProfile(ctx, keyData)
|
|
require.NoError(t, err)
|
|
|
|
setImpersonationPolicy(t, instance, true)
|
|
|
|
iamUserID, iamImpersonatorPAT := createMachineUserPATWithMembership(ctx, t, instance, "IAM_ADMIN_IMPERSONATOR")
|
|
iamOwner := instance.Users.Get(integration.UserTypeIAMOwner)
|
|
|
|
// impersonating the instance owner!
|
|
resp, err := tokenexchange.ExchangeToken(ctx, exchanger, iamOwner.Token, oidc.AccessTokenType, iamImpersonatorPAT, oidc.AccessTokenType, nil, nil, nil, oidc.AccessTokenType)
|
|
require.NoError(t, err)
|
|
accessTokenVerifier(ctx, resourceServer, iamOwner.ID, iamUserID)
|
|
|
|
impersonatedCTX := integration.WithAuthorizationToken(ctx, resp.AccessToken)
|
|
_, err = instance.Client.Admin.GetAllowedLanguages(impersonatedCTX, &admin.GetAllowedLanguagesRequest{})
|
|
status := status.Convert(err)
|
|
assert.Equal(t, codes.PermissionDenied, status.Code())
|
|
assert.Equal(t, "Errors.TokenExchange.Token.NotForAPI (APP-Shi0J)", status.Message())
|
|
}
|