mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 01:37:31 +00:00
feat: actions v2 for functions (#9420)
# Which Problems Are Solved Actions v2 are not executed in different functions, as provided by the actions v1. # How the Problems Are Solved Add functionality to call actions v2 through OIDC and SAML logic to complement tokens and SAMLResponses. # Additional Changes - Corrected testing for retrieved intent information - Added testing for IDP types - Corrected handling of context for issuer in SAML logic # Additional Context - Closes #7247 - Dependent on https://github.com/zitadel/saml/pull/97 - docs for migration are done in separate issue: https://github.com/zitadel/zitadel/issues/9456 --------- Co-authored-by: Silvan <27845747+adlerhurst@users.noreply.github.com>
This commit is contained in:
@@ -4,25 +4,54 @@ package action_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/brianvoe/gofakeit/v6"
|
||||
"github.com/crewjam/saml"
|
||||
"github.com/crewjam/saml/samlsp"
|
||||
"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"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
"golang.org/x/text/language"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server/middleware"
|
||||
oidc_api "github.com/zitadel/zitadel/internal/api/oidc"
|
||||
saml_api "github.com/zitadel/zitadel/internal/api/saml"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/app"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/management"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/metadata"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
|
||||
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
|
||||
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
|
||||
saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
redirectURI = "https://callback"
|
||||
logoutRedirectURI = "https://logged-out"
|
||||
redirectURIImplicit = "http://localhost:9999/callback"
|
||||
)
|
||||
|
||||
var (
|
||||
loginV2 = &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: nil}}}
|
||||
)
|
||||
|
||||
func TestServer_ExecutionTarget(t *testing.T) {
|
||||
@@ -408,3 +437,749 @@ func testServerCall(
|
||||
|
||||
return server.URL, server.Close
|
||||
}
|
||||
|
||||
func conditionFunction(function string) *action.Condition {
|
||||
return &action.Condition{
|
||||
ConditionType: &action.Condition_Function{
|
||||
Function: &action.FunctionExecution{
|
||||
Name: function,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_ExecutionTargetPreUserinfo(t *testing.T) {
|
||||
instance := integration.NewInstance(CTX)
|
||||
ensureFeatureEnabled(t, instance)
|
||||
isolatedIAMCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
|
||||
ctxLoginClient := instance.WithAuthorization(CTX, integration.UserTypeLogin)
|
||||
|
||||
client, err := instance.CreateOIDCImplicitFlowClient(isolatedIAMCtx, redirectURIImplicit, loginV2)
|
||||
require.NoError(t, err)
|
||||
|
||||
type want struct {
|
||||
addedClaims map[string]any
|
||||
addedLogClaims map[string][]string
|
||||
setUserMetadata []*metadata.Metadata
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
dep func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func())
|
||||
req *oidc_pb.CreateCallbackRequest
|
||||
want want
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "append claim",
|
||||
ctx: ctxLoginClient,
|
||||
dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) {
|
||||
response := &oidc_api.ContextInfoResponse{
|
||||
AppendClaims: []*oidc_api.AppendClaim{
|
||||
{Key: "added", Value: "value"},
|
||||
},
|
||||
}
|
||||
return expectPreUserinfoExecution(ctx, t, instance, req, response)
|
||||
},
|
||||
req: &oidc_pb.CreateCallbackRequest{
|
||||
AuthRequestId: func() string {
|
||||
authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit)
|
||||
require.NoError(t, err)
|
||||
return authRequestID
|
||||
}(),
|
||||
},
|
||||
want: want{
|
||||
addedClaims: map[string]any{
|
||||
"added": "value",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "append log claim",
|
||||
ctx: ctxLoginClient,
|
||||
dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) {
|
||||
response := &oidc_api.ContextInfoResponse{
|
||||
AppendLogClaims: []string{
|
||||
"addedLog",
|
||||
},
|
||||
}
|
||||
return expectPreUserinfoExecution(ctx, t, instance, req, response)
|
||||
},
|
||||
req: &oidc_pb.CreateCallbackRequest{
|
||||
AuthRequestId: func() string {
|
||||
authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit)
|
||||
require.NoError(t, err)
|
||||
return authRequestID
|
||||
}(),
|
||||
},
|
||||
want: want{
|
||||
addedLogClaims: map[string][]string{
|
||||
"urn:zitadel:iam:action:function/preuserinfo:log": {"addedLog"},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "set user metadata",
|
||||
ctx: ctxLoginClient,
|
||||
dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) {
|
||||
response := &oidc_api.ContextInfoResponse{
|
||||
SetUserMetadata: []*domain.Metadata{
|
||||
{Key: "key", Value: []byte("value")},
|
||||
},
|
||||
}
|
||||
return expectPreUserinfoExecution(ctx, t, instance, req, response)
|
||||
},
|
||||
req: &oidc_pb.CreateCallbackRequest{
|
||||
AuthRequestId: func() string {
|
||||
authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit)
|
||||
require.NoError(t, err)
|
||||
return authRequestID
|
||||
}(),
|
||||
},
|
||||
want: want{
|
||||
setUserMetadata: []*metadata.Metadata{
|
||||
{Key: "key", Value: []byte("value")},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "full usage",
|
||||
ctx: ctxLoginClient,
|
||||
dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) {
|
||||
response := &oidc_api.ContextInfoResponse{
|
||||
SetUserMetadata: []*domain.Metadata{
|
||||
{Key: "key1", Value: []byte("value1")},
|
||||
{Key: "key2", Value: []byte("value2")},
|
||||
{Key: "key3", Value: []byte("value3")},
|
||||
},
|
||||
AppendLogClaims: []string{
|
||||
"addedLog1",
|
||||
"addedLog2",
|
||||
"addedLog3",
|
||||
},
|
||||
AppendClaims: []*oidc_api.AppendClaim{
|
||||
{Key: "added1", Value: "value1"},
|
||||
{Key: "added2", Value: "value2"},
|
||||
{Key: "added3", Value: "value3"},
|
||||
},
|
||||
}
|
||||
return expectPreUserinfoExecution(ctx, t, instance, req, response)
|
||||
},
|
||||
req: &oidc_pb.CreateCallbackRequest{
|
||||
AuthRequestId: func() string {
|
||||
authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit)
|
||||
require.NoError(t, err)
|
||||
return authRequestID
|
||||
}(),
|
||||
},
|
||||
want: want{
|
||||
addedClaims: map[string]any{
|
||||
"added1": "value1",
|
||||
"added2": "value2",
|
||||
"added3": "value3",
|
||||
},
|
||||
setUserMetadata: []*metadata.Metadata{
|
||||
{Key: "key1", Value: []byte("value1")},
|
||||
{Key: "key2", Value: []byte("value2")},
|
||||
{Key: "key3", Value: []byte("value3")},
|
||||
},
|
||||
addedLogClaims: map[string][]string{
|
||||
"urn:zitadel:iam:action:function/preuserinfo:log": {"addedLog1", "addedLog2", "addedLog3"},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
userID, closeF := tt.dep(isolatedIAMCtx, t, tt.req)
|
||||
defer closeF()
|
||||
|
||||
got, err := instance.Client.OIDCv2.CreateCallback(tt.ctx, tt.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
callbackUrl, err := url.Parse(strings.Replace(got.GetCallbackUrl(), "#", "?", 1))
|
||||
require.NoError(t, err)
|
||||
claims := getIDTokenClaimsFromCallbackURL(tt.ctx, t, instance, client.GetClientId(), callbackUrl)
|
||||
|
||||
for k, v := range tt.want.addedClaims {
|
||||
value, ok := claims[k]
|
||||
if !assert.True(t, ok) {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, v, value)
|
||||
}
|
||||
for k, v := range tt.want.addedLogClaims {
|
||||
value, ok := claims[k]
|
||||
if !assert.True(t, ok) {
|
||||
return
|
||||
}
|
||||
assert.ElementsMatch(t, v, value)
|
||||
}
|
||||
if len(tt.want.setUserMetadata) > 0 {
|
||||
checkForSetMetadata(isolatedIAMCtx, t, instance, userID, tt.want.setUserMetadata)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func expectPreUserinfoExecution(ctx context.Context, t *testing.T, instance *integration.Instance, req *oidc_pb.CreateCallbackRequest, response *oidc_api.ContextInfoResponse) (string, func()) {
|
||||
userEmail := gofakeit.Email()
|
||||
userPhone := "+41" + gofakeit.Phone()
|
||||
userResp := instance.CreateHumanUserVerified(ctx, instance.DefaultOrg.Id, userEmail, userPhone)
|
||||
|
||||
sessionResp := createSession(ctx, t, instance, userResp.GetUserId())
|
||||
req.CallbackKind = &oidc_pb.CreateCallbackRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: sessionResp.GetSessionId(),
|
||||
SessionToken: sessionResp.GetSessionToken(),
|
||||
},
|
||||
}
|
||||
expectedContextInfo := contextInfoForUserOIDC(instance, "function/preuserinfo", userResp, userEmail, userPhone)
|
||||
|
||||
targetURL, closeF := testServerCall(expectedContextInfo, 0, http.StatusOK, response)
|
||||
|
||||
targetResp := waitForTarget(ctx, t, instance, targetURL, domain.TargetTypeCall, true)
|
||||
waitForExecutionOnCondition(ctx, t, instance, conditionFunction("preuserinfo"), executionTargetsSingleTarget(targetResp.GetDetails().GetId()))
|
||||
return userResp.GetUserId(), closeF
|
||||
}
|
||||
|
||||
func createSession(ctx context.Context, t *testing.T, instance *integration.Instance, userID string) *session.CreateSessionResponse {
|
||||
sessionResp, err := instance.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
UserId: userID,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return sessionResp
|
||||
}
|
||||
|
||||
func checkForSetMetadata(ctx context.Context, t *testing.T, instance *integration.Instance, userID string, metadataExpected []*metadata.Metadata) {
|
||||
integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
|
||||
|
||||
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
metadataResp, err := instance.Client.Mgmt.ListUserMetadata(ctx, &management.ListUserMetadataRequest{Id: userID})
|
||||
if !assert.NoError(ct, err) {
|
||||
return
|
||||
}
|
||||
for _, dataExpected := range metadataExpected {
|
||||
found := false
|
||||
for _, dataCheck := range metadataResp.GetResult() {
|
||||
if dataExpected.Key == dataCheck.Key {
|
||||
found = true
|
||||
if !assert.Equal(ct, dataExpected.Value, dataCheck.Value) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if !assert.True(ct, found) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}, retryDuration, tick)
|
||||
}
|
||||
|
||||
func getIDTokenClaimsFromCallbackURL(ctx context.Context, t *testing.T, instance *integration.Instance, clientID string, callbackURL *url.URL) map[string]any {
|
||||
accessToken := callbackURL.Query().Get("access_token")
|
||||
idToken := callbackURL.Query().Get("id_token")
|
||||
|
||||
provider, err := instance.CreateRelyingParty(ctx, clientID, redirectURIImplicit, oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopePhone)
|
||||
require.NoError(t, err)
|
||||
claims, err := rp.VerifyTokens[*oidc.IDTokenClaims](context.Background(), accessToken, idToken, provider.IDTokenVerifier())
|
||||
require.NoError(t, err)
|
||||
return claims.Claims
|
||||
}
|
||||
|
||||
type CustomAccessTokenClaims struct {
|
||||
oidc.TokenClaims
|
||||
Added1 string `json:"added1,omitempty"`
|
||||
Added2 string `json:"added2,omitempty"`
|
||||
Added3 string `json:"added3,omitempty"`
|
||||
Log []string `json:"urn:zitadel:iam:action:function/preaccesstoken:log,omitempty"`
|
||||
}
|
||||
|
||||
func getAccessTokenClaims(ctx context.Context, t *testing.T, instance *integration.Instance, callbackURL *url.URL) *CustomAccessTokenClaims {
|
||||
accessToken := callbackURL.Query().Get("access_token")
|
||||
|
||||
verifier := op.NewAccessTokenVerifier(instance.OIDCIssuer(), rp.NewRemoteKeySet(http.DefaultClient, instance.OIDCIssuer()+"/oauth/v2/keys"))
|
||||
|
||||
claims, err := op.VerifyAccessToken[*CustomAccessTokenClaims](ctx, accessToken, verifier)
|
||||
require.NoError(t, err)
|
||||
return claims
|
||||
}
|
||||
|
||||
func contextInfoForUserOIDC(instance *integration.Instance, function string, userResp *user.AddHumanUserResponse, email, phone string) *oidc_api.ContextInfo {
|
||||
return &oidc_api.ContextInfo{
|
||||
Function: function,
|
||||
UserInfo: &oidc.UserInfo{
|
||||
Subject: userResp.GetUserId(),
|
||||
},
|
||||
User: &query.User{
|
||||
ID: userResp.GetUserId(),
|
||||
CreationDate: userResp.Details.ChangeDate.AsTime(),
|
||||
ChangeDate: userResp.Details.ChangeDate.AsTime(),
|
||||
ResourceOwner: instance.DefaultOrg.GetId(),
|
||||
Sequence: userResp.Details.Sequence,
|
||||
State: 1,
|
||||
Username: email,
|
||||
PreferredLoginName: email,
|
||||
Human: &query.Human{
|
||||
FirstName: "Mickey",
|
||||
LastName: "Mouse",
|
||||
NickName: "Mickey",
|
||||
DisplayName: "Mickey Mouse",
|
||||
AvatarKey: "",
|
||||
PreferredLanguage: language.Dutch,
|
||||
Gender: 2,
|
||||
Email: domain.EmailAddress(email),
|
||||
IsEmailVerified: true,
|
||||
Phone: domain.PhoneNumber(phone),
|
||||
IsPhoneVerified: true,
|
||||
PasswordChangeRequired: false,
|
||||
PasswordChanged: time.Time{},
|
||||
MFAInitSkipped: time.Time{},
|
||||
},
|
||||
},
|
||||
UserMetadata: nil,
|
||||
Org: &query.UserInfoOrg{
|
||||
ID: instance.DefaultOrg.GetId(),
|
||||
Name: instance.DefaultOrg.GetName(),
|
||||
PrimaryDomain: instance.DefaultOrg.GetPrimaryDomain(),
|
||||
},
|
||||
UserGrants: nil,
|
||||
Response: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_ExecutionTargetPreAccessToken(t *testing.T) {
|
||||
instance := integration.NewInstance(CTX)
|
||||
ensureFeatureEnabled(t, instance)
|
||||
isolatedIAMCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
|
||||
ctxLoginClient := instance.WithAuthorization(CTX, integration.UserTypeLogin)
|
||||
|
||||
client, err := instance.CreateOIDCImplicitFlowClient(isolatedIAMCtx, redirectURIImplicit, loginV2)
|
||||
require.NoError(t, err)
|
||||
|
||||
type want struct {
|
||||
addedClaims *CustomAccessTokenClaims
|
||||
addedLogClaims map[string][]string
|
||||
setUserMetadata []*metadata.Metadata
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
dep func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func())
|
||||
req *oidc_pb.CreateCallbackRequest
|
||||
want want
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "append claim",
|
||||
ctx: ctxLoginClient,
|
||||
dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) {
|
||||
response := &oidc_api.ContextInfoResponse{
|
||||
AppendClaims: []*oidc_api.AppendClaim{
|
||||
{Key: "added1", Value: "value"},
|
||||
},
|
||||
}
|
||||
return expectPreAccessTokenExecution(ctx, t, instance, req, response)
|
||||
},
|
||||
req: &oidc_pb.CreateCallbackRequest{
|
||||
AuthRequestId: func() string {
|
||||
authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit)
|
||||
require.NoError(t, err)
|
||||
return authRequestID
|
||||
}(),
|
||||
},
|
||||
want: want{
|
||||
addedClaims: &CustomAccessTokenClaims{
|
||||
Added1: "value",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "append log claim",
|
||||
ctx: ctxLoginClient,
|
||||
dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) {
|
||||
response := &oidc_api.ContextInfoResponse{
|
||||
AppendLogClaims: []string{
|
||||
"addedLog",
|
||||
},
|
||||
}
|
||||
return expectPreAccessTokenExecution(ctx, t, instance, req, response)
|
||||
},
|
||||
req: &oidc_pb.CreateCallbackRequest{
|
||||
AuthRequestId: func() string {
|
||||
authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit)
|
||||
require.NoError(t, err)
|
||||
return authRequestID
|
||||
}(),
|
||||
},
|
||||
want: want{
|
||||
addedClaims: &CustomAccessTokenClaims{
|
||||
Log: []string{"addedLog"},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "set user metadata",
|
||||
ctx: ctxLoginClient,
|
||||
dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) {
|
||||
response := &oidc_api.ContextInfoResponse{
|
||||
SetUserMetadata: []*domain.Metadata{
|
||||
{Key: "key", Value: []byte("value")},
|
||||
},
|
||||
}
|
||||
return expectPreAccessTokenExecution(ctx, t, instance, req, response)
|
||||
},
|
||||
req: &oidc_pb.CreateCallbackRequest{
|
||||
AuthRequestId: func() string {
|
||||
authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit)
|
||||
require.NoError(t, err)
|
||||
return authRequestID
|
||||
}(),
|
||||
},
|
||||
want: want{
|
||||
setUserMetadata: []*metadata.Metadata{
|
||||
{Key: "key", Value: []byte("value")},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "full usage",
|
||||
ctx: ctxLoginClient,
|
||||
dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) {
|
||||
response := &oidc_api.ContextInfoResponse{
|
||||
SetUserMetadata: []*domain.Metadata{
|
||||
{Key: "key1", Value: []byte("value1")},
|
||||
{Key: "key2", Value: []byte("value2")},
|
||||
{Key: "key3", Value: []byte("value3")},
|
||||
},
|
||||
AppendLogClaims: []string{
|
||||
"addedLog1",
|
||||
"addedLog2",
|
||||
"addedLog3",
|
||||
},
|
||||
AppendClaims: []*oidc_api.AppendClaim{
|
||||
{Key: "added1", Value: "value1"},
|
||||
{Key: "added2", Value: "value2"},
|
||||
{Key: "added3", Value: "value3"},
|
||||
},
|
||||
}
|
||||
return expectPreAccessTokenExecution(ctx, t, instance, req, response)
|
||||
},
|
||||
req: &oidc_pb.CreateCallbackRequest{
|
||||
AuthRequestId: func() string {
|
||||
authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit)
|
||||
require.NoError(t, err)
|
||||
return authRequestID
|
||||
}(),
|
||||
},
|
||||
want: want{
|
||||
addedClaims: &CustomAccessTokenClaims{
|
||||
Added1: "value1",
|
||||
Added2: "value2",
|
||||
Added3: "value3",
|
||||
Log: []string{"addedLog1", "addedLog2", "addedLog3"},
|
||||
},
|
||||
setUserMetadata: []*metadata.Metadata{
|
||||
{Key: "key1", Value: []byte("value1")},
|
||||
{Key: "key2", Value: []byte("value2")},
|
||||
{Key: "key3", Value: []byte("value3")},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
userID, closeF := tt.dep(isolatedIAMCtx, t, tt.req)
|
||||
defer closeF()
|
||||
|
||||
got, err := instance.Client.OIDCv2.CreateCallback(tt.ctx, tt.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
callbackUrl, err := url.Parse(strings.Replace(got.GetCallbackUrl(), "#", "?", 1))
|
||||
require.NoError(t, err)
|
||||
claims := getAccessTokenClaims(tt.ctx, t, instance, callbackUrl)
|
||||
|
||||
if tt.want.addedClaims != nil {
|
||||
assert.Equal(t, tt.want.addedClaims.Added1, claims.Added1)
|
||||
assert.Equal(t, tt.want.addedClaims.Added2, claims.Added2)
|
||||
assert.Equal(t, tt.want.addedClaims.Added3, claims.Added3)
|
||||
assert.Equal(t, tt.want.addedClaims.Log, claims.Log)
|
||||
}
|
||||
if len(tt.want.setUserMetadata) > 0 {
|
||||
checkForSetMetadata(isolatedIAMCtx, t, instance, userID, tt.want.setUserMetadata)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func expectPreAccessTokenExecution(ctx context.Context, t *testing.T, instance *integration.Instance, req *oidc_pb.CreateCallbackRequest, response *oidc_api.ContextInfoResponse) (string, func()) {
|
||||
userEmail := gofakeit.Email()
|
||||
userPhone := "+41" + gofakeit.Phone()
|
||||
userResp := instance.CreateHumanUserVerified(ctx, instance.DefaultOrg.Id, userEmail, userPhone)
|
||||
|
||||
sessionResp := createSession(ctx, t, instance, userResp.GetUserId())
|
||||
req.CallbackKind = &oidc_pb.CreateCallbackRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: sessionResp.GetSessionId(),
|
||||
SessionToken: sessionResp.GetSessionToken(),
|
||||
},
|
||||
}
|
||||
expectedContextInfo := contextInfoForUserOIDC(instance, "function/preaccesstoken", userResp, userEmail, userPhone)
|
||||
|
||||
targetURL, closeF := testServerCall(expectedContextInfo, 0, http.StatusOK, response)
|
||||
|
||||
targetResp := waitForTarget(ctx, t, instance, targetURL, domain.TargetTypeCall, true)
|
||||
waitForExecutionOnCondition(ctx, t, instance, conditionFunction("preaccesstoken"), executionTargetsSingleTarget(targetResp.GetDetails().GetId()))
|
||||
return userResp.GetUserId(), closeF
|
||||
}
|
||||
|
||||
func TestServer_ExecutionTargetPreSAMLResponse(t *testing.T) {
|
||||
instance := integration.NewInstance(CTX)
|
||||
ensureFeatureEnabled(t, instance)
|
||||
isolatedIAMCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
|
||||
ctxLoginClient := instance.WithAuthorization(CTX, integration.UserTypeLogin)
|
||||
|
||||
idpMetadata, err := instance.GetSAMLIDPMetadata()
|
||||
require.NoError(t, err)
|
||||
|
||||
acsPost := idpMetadata.IDPSSODescriptors[0].SingleSignOnServices[1]
|
||||
_, _, spMiddlewarePost := createSAMLApplication(isolatedIAMCtx, t, instance, idpMetadata, saml.HTTPPostBinding, false, false)
|
||||
|
||||
type want struct {
|
||||
addedAttributes map[string][]saml.AttributeValue
|
||||
setUserMetadata []*metadata.Metadata
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
dep func(ctx context.Context, t *testing.T, req *saml_pb.CreateResponseRequest) (string, func())
|
||||
req *saml_pb.CreateResponseRequest
|
||||
want want
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "append attribute",
|
||||
ctx: ctxLoginClient,
|
||||
dep: func(ctx context.Context, t *testing.T, req *saml_pb.CreateResponseRequest) (string, func()) {
|
||||
response := &saml_api.ContextInfoResponse{
|
||||
AppendAttribute: []*saml_api.AppendAttribute{
|
||||
{Name: "added", NameFormat: "format", Value: []string{"value"}},
|
||||
},
|
||||
}
|
||||
return expectPreSAMLResponseExecution(ctx, t, instance, req, response)
|
||||
},
|
||||
req: &saml_pb.CreateResponseRequest{
|
||||
SamlRequestId: func() string {
|
||||
_, samlRequestID, err := instance.CreateSAMLAuthRequest(spMiddlewarePost, instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding)
|
||||
require.NoError(t, err)
|
||||
return samlRequestID
|
||||
}(),
|
||||
},
|
||||
want: want{
|
||||
addedAttributes: map[string][]saml.AttributeValue{
|
||||
"added": {saml.AttributeValue{Value: "value"}},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "set user metadata",
|
||||
ctx: ctxLoginClient,
|
||||
dep: func(ctx context.Context, t *testing.T, req *saml_pb.CreateResponseRequest) (string, func()) {
|
||||
response := &saml_api.ContextInfoResponse{
|
||||
SetUserMetadata: []*domain.Metadata{
|
||||
{Key: "key", Value: []byte("value")},
|
||||
},
|
||||
}
|
||||
return expectPreSAMLResponseExecution(ctx, t, instance, req, response)
|
||||
},
|
||||
req: &saml_pb.CreateResponseRequest{
|
||||
SamlRequestId: func() string {
|
||||
_, samlRequestID, err := instance.CreateSAMLAuthRequest(spMiddlewarePost, instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding)
|
||||
require.NoError(t, err)
|
||||
return samlRequestID
|
||||
}(),
|
||||
},
|
||||
want: want{
|
||||
setUserMetadata: []*metadata.Metadata{
|
||||
{Key: "key", Value: []byte("value")},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "set user metadata",
|
||||
ctx: ctxLoginClient,
|
||||
dep: func(ctx context.Context, t *testing.T, req *saml_pb.CreateResponseRequest) (string, func()) {
|
||||
response := &saml_api.ContextInfoResponse{
|
||||
AppendAttribute: []*saml_api.AppendAttribute{
|
||||
{Name: "added1", NameFormat: "format", Value: []string{"value1"}},
|
||||
{Name: "added2", NameFormat: "format", Value: []string{"value2"}},
|
||||
{Name: "added3", NameFormat: "format", Value: []string{"value3"}},
|
||||
},
|
||||
SetUserMetadata: []*domain.Metadata{
|
||||
{Key: "key1", Value: []byte("value1")},
|
||||
{Key: "key2", Value: []byte("value2")},
|
||||
{Key: "key3", Value: []byte("value3")},
|
||||
},
|
||||
}
|
||||
return expectPreSAMLResponseExecution(ctx, t, instance, req, response)
|
||||
},
|
||||
req: &saml_pb.CreateResponseRequest{
|
||||
SamlRequestId: func() string {
|
||||
_, samlRequestID, err := instance.CreateSAMLAuthRequest(spMiddlewarePost, instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding)
|
||||
require.NoError(t, err)
|
||||
return samlRequestID
|
||||
}(),
|
||||
},
|
||||
want: want{
|
||||
addedAttributes: map[string][]saml.AttributeValue{
|
||||
"added1": {saml.AttributeValue{Value: "value1"}},
|
||||
"added2": {saml.AttributeValue{Value: "value2"}},
|
||||
"added3": {saml.AttributeValue{Value: "value3"}},
|
||||
},
|
||||
setUserMetadata: []*metadata.Metadata{
|
||||
{Key: "key1", Value: []byte("value1")},
|
||||
{Key: "key2", Value: []byte("value2")},
|
||||
{Key: "key3", Value: []byte("value3")},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
userID, closeF := tt.dep(isolatedIAMCtx, t, tt.req)
|
||||
defer closeF()
|
||||
|
||||
got, err := instance.Client.SAMLv2.CreateResponse(tt.ctx, tt.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
attributes := getSAMLResponseAttributes(t, got.GetPost().GetSamlResponse(), spMiddlewarePost)
|
||||
for k, v := range tt.want.addedAttributes {
|
||||
found := false
|
||||
for _, attribute := range attributes {
|
||||
if attribute.Name == k {
|
||||
found = true
|
||||
assert.Equal(t, v, attribute.Values)
|
||||
}
|
||||
}
|
||||
if !assert.True(t, found) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if len(tt.want.setUserMetadata) > 0 {
|
||||
checkForSetMetadata(isolatedIAMCtx, t, instance, userID, tt.want.setUserMetadata)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func expectPreSAMLResponseExecution(ctx context.Context, t *testing.T, instance *integration.Instance, req *saml_pb.CreateResponseRequest, response *saml_api.ContextInfoResponse) (string, func()) {
|
||||
userEmail := gofakeit.Email()
|
||||
userPhone := "+41" + gofakeit.Phone()
|
||||
userResp := instance.CreateHumanUserVerified(ctx, instance.DefaultOrg.Id, userEmail, userPhone)
|
||||
|
||||
sessionResp := createSession(ctx, t, instance, userResp.GetUserId())
|
||||
req.ResponseKind = &saml_pb.CreateResponseRequest_Session{
|
||||
Session: &saml_pb.Session{
|
||||
SessionId: sessionResp.GetSessionId(),
|
||||
SessionToken: sessionResp.GetSessionToken(),
|
||||
},
|
||||
}
|
||||
expectedContextInfo := contextInfoForUserSAML(instance, "function/presamlresponse", userResp, userEmail, userPhone)
|
||||
|
||||
targetURL, closeF := testServerCall(expectedContextInfo, 0, http.StatusOK, response)
|
||||
|
||||
targetResp := waitForTarget(ctx, t, instance, targetURL, domain.TargetTypeCall, true)
|
||||
waitForExecutionOnCondition(ctx, t, instance, conditionFunction("presamlresponse"), executionTargetsSingleTarget(targetResp.GetDetails().GetId()))
|
||||
|
||||
return userResp.GetUserId(), closeF
|
||||
}
|
||||
|
||||
func createSAMLSP(t *testing.T, idpMetadata *saml.EntityDescriptor, binding string) (string, *samlsp.Middleware) {
|
||||
rootURL := "example." + gofakeit.DomainName()
|
||||
spMiddleware, err := integration.CreateSAMLSP("https://"+rootURL, idpMetadata, binding)
|
||||
require.NoError(t, err)
|
||||
return rootURL, spMiddleware
|
||||
}
|
||||
|
||||
func createSAMLApplication(ctx context.Context, t *testing.T, instance *integration.Instance, idpMetadata *saml.EntityDescriptor, binding string, projectRoleCheck, hasProjectCheck bool) (string, string, *samlsp.Middleware) {
|
||||
project, err := instance.CreateProjectWithPermissionCheck(ctx, projectRoleCheck, hasProjectCheck)
|
||||
require.NoError(t, err)
|
||||
rootURL, sp := createSAMLSP(t, idpMetadata, binding)
|
||||
_, err = instance.CreateSAMLClient(ctx, project.GetId(), sp)
|
||||
require.NoError(t, err)
|
||||
return project.GetId(), rootURL, sp
|
||||
}
|
||||
|
||||
func getSAMLResponseAttributes(t *testing.T, samlResponse string, sp *samlsp.Middleware) []saml.Attribute {
|
||||
data, err := base64.StdEncoding.DecodeString(samlResponse)
|
||||
require.NoError(t, err)
|
||||
sp.ServiceProvider.AllowIDPInitiated = true
|
||||
assertion, err := sp.ServiceProvider.ParseXMLResponse(data, []string{})
|
||||
require.NoError(t, err)
|
||||
return assertion.AttributeStatements[0].Attributes
|
||||
}
|
||||
|
||||
func contextInfoForUserSAML(instance *integration.Instance, function string, userResp *user.AddHumanUserResponse, email, phone string) *saml_api.ContextInfo {
|
||||
return &saml_api.ContextInfo{
|
||||
Function: function,
|
||||
User: &query.User{
|
||||
ID: userResp.GetUserId(),
|
||||
CreationDate: userResp.Details.ChangeDate.AsTime(),
|
||||
ChangeDate: userResp.Details.ChangeDate.AsTime(),
|
||||
ResourceOwner: instance.DefaultOrg.GetId(),
|
||||
Sequence: userResp.Details.Sequence,
|
||||
State: 1,
|
||||
Type: domain.UserTypeHuman,
|
||||
Username: email,
|
||||
PreferredLoginName: email,
|
||||
LoginNames: []string{email},
|
||||
Human: &query.Human{
|
||||
FirstName: "Mickey",
|
||||
LastName: "Mouse",
|
||||
NickName: "Mickey",
|
||||
DisplayName: "Mickey Mouse",
|
||||
AvatarKey: "",
|
||||
PreferredLanguage: language.Dutch,
|
||||
Gender: 2,
|
||||
Email: domain.EmailAddress(email),
|
||||
IsEmailVerified: true,
|
||||
Phone: domain.PhoneNumber(phone),
|
||||
IsPhoneVerified: true,
|
||||
PasswordChangeRequired: false,
|
||||
PasswordChanged: time.Time{},
|
||||
MFAInitSkipped: time.Time{},
|
||||
},
|
||||
},
|
||||
UserGrants: nil,
|
||||
Response: nil,
|
||||
}
|
||||
}
|
||||
|
@@ -774,7 +774,7 @@ func TestServer_SetExecution_Function(t *testing.T) {
|
||||
req: &action.SetExecutionRequest{
|
||||
Condition: &action.Condition{
|
||||
ConditionType: &action.Condition_Function{
|
||||
Function: &action.FunctionExecution{Name: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication"},
|
||||
Function: &action.FunctionExecution{Name: "presamlresponse"},
|
||||
},
|
||||
},
|
||||
Execution: &action.Execution{
|
||||
|
@@ -835,7 +835,7 @@ func TestServer_SearchExecutions(t *testing.T) {
|
||||
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: "user.added"}}}},
|
||||
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: "user"}}}},
|
||||
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}},
|
||||
{ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication"}}},
|
||||
{ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: "presamlresponse"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
Reference in New Issue
Block a user