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:
Stefan Benz
2025-03-04 12:09:30 +01:00
committed by GitHub
parent d9d8339813
commit 0c87a96e2c
29 changed files with 1964 additions and 380 deletions

View File

@@ -104,13 +104,11 @@ func TestServer_CreateCallback(t *testing.T) {
sessionResp := createSession(t, CTX, Instance.Users[integration.UserTypeOrgOwner].ID)
tests := []struct {
name string
ctx context.Context
req *oidc_pb.CreateCallbackRequest
AuthError string
want *oidc_pb.CreateCallbackResponse
wantURL *url.URL
wantErr bool
name string
ctx context.Context
req *oidc_pb.CreateCallbackRequest
want *oidc_pb.CreateCallbackResponse
wantErr bool
}{
{
name: "Not found",

View File

@@ -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,
}
}

View File

@@ -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{

View File

@@ -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"}}},
},
},
},

View File

@@ -3,23 +3,19 @@ package middleware
import (
"context"
"encoding/json"
"strings"
"github.com/zitadel/logging"
"google.golang.org/grpc"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/execution"
"github.com/zitadel/zitadel/internal/query"
exec_repo "github.com/zitadel/zitadel/internal/repository/execution"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
func ExecutionHandler(queries *query.Queries) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
requestTargets, responseTargets := queryTargets(ctx, queries, info.FullMethod)
requestTargets, responseTargets := execution.QueryExecutionTargetsForRequestAndResponse(ctx, queries, info.FullMethod)
// call targets otherwise return req
handledReq, err := executeTargetsForRequest(ctx, requestTargets, info.FullMethod, req)
@@ -81,49 +77,6 @@ func executeTargetsForResponse(ctx context.Context, targets []execution.Target,
return execution.CallTargets(ctx, targets, info)
}
type ExecutionQueries interface {
TargetsByExecutionIDs(ctx context.Context, ids1, ids2 []string) (execution []*query.ExecutionTarget, err error)
}
func queryTargets(
ctx context.Context,
queries ExecutionQueries,
fullMethod string,
) ([]execution.Target, []execution.Target) {
ctx, span := tracing.NewSpan(ctx)
defer span.End()
targets, err := queries.TargetsByExecutionIDs(ctx,
idsForFullMethod(fullMethod, domain.ExecutionTypeRequest),
idsForFullMethod(fullMethod, domain.ExecutionTypeResponse),
)
requestTargets := make([]execution.Target, 0, len(targets))
responseTargets := make([]execution.Target, 0, len(targets))
if err != nil {
logging.WithFields("fullMethod", fullMethod).WithError(err).Info("unable to query targets")
return requestTargets, responseTargets
}
for _, target := range targets {
if strings.HasPrefix(target.GetExecutionID(), exec_repo.IDAll(domain.ExecutionTypeRequest)) {
requestTargets = append(requestTargets, target)
} else if strings.HasPrefix(target.GetExecutionID(), exec_repo.IDAll(domain.ExecutionTypeResponse)) {
responseTargets = append(responseTargets, target)
}
}
return requestTargets, responseTargets
}
func idsForFullMethod(fullMethod string, executionType domain.ExecutionType) []string {
return []string{exec_repo.ID(executionType, fullMethod), exec_repo.ID(executionType, serviceFromFullMethod(fullMethod)), exec_repo.IDAll(executionType)}
}
func serviceFromFullMethod(s string) string {
parts := strings.Split(s, "/")
return parts[1]
}
var _ execution.ContextInfo = &ContextInfoRequest{}
type ContextInfoRequest struct {

View File

@@ -20,7 +20,6 @@ import (
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/grpc"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/internal/integration/sink"
"github.com/zitadel/zitadel/pkg/grpc/auth"
@@ -2114,18 +2113,29 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
}
func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
idpID := Instance.AddGenericOAuthProvider(IamCTX, gofakeit.AppName()).GetId()
intentID := Instance.CreateIntent(CTX, idpID).GetIdpIntent().GetIdpIntentId()
oauthIdpID := Instance.AddGenericOAuthProvider(IamCTX, gofakeit.AppName()).GetId()
oidcIdpID := Instance.AddGenericOIDCProvider(IamCTX, gofakeit.AppName()).GetId()
samlIdpID := Instance.AddSAMLPostProvider(IamCTX)
ldapIdpID := Instance.AddLDAPProvider(IamCTX)
authURL, err := url.Parse(Instance.CreateIntent(CTX, oauthIdpID).GetAuthUrl())
require.NoError(t, err)
intentID := authURL.Query().Get("state")
successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", "")
successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "")
require.NoError(t, err)
successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", "user")
successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user")
require.NoError(t, err)
ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), idpID, "id", "")
oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "")
require.NoError(t, err)
ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), idpID, "id", "user")
oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user")
require.NoError(t, err)
samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), idpID, "id", "")
ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "")
require.NoError(t, err)
ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "user")
require.NoError(t, err)
samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "")
require.NoError(t, err)
samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user")
require.NoError(t, err)
type args struct {
ctx context.Context
@@ -2160,7 +2170,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
wantErr: true,
},
{
name: "retrieve successful intent",
name: "retrieve successful oauth intent",
args: args{
CTX,
&user.RetrieveIdentityProviderIntentRequest{
@@ -2181,18 +2191,31 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
IdToken: gu.Ptr("idToken"),
},
},
IdpId: idpID,
IdpId: oauthIdpID,
UserId: "id",
UserName: "username",
UserName: "",
RawInformation: func() *structpb.Struct {
s, err := structpb.NewStruct(map[string]interface{}{
"sub": "id",
"preferred_username": "username",
"RawInfo": map[string]interface{}{
"id": "id",
"preferred_username": "username",
},
})
require.NoError(t, err)
return s
}(),
},
AddHumanUser: &user.AddHumanUserRequest{
Profile: &user.SetHumanProfile{
PreferredLanguage: gu.Ptr("und"),
},
IdpLinks: []*user.IDPLink{
{IdpId: oauthIdpID, UserId: "id"},
},
Email: &user.SetHumanEmail{
Verification: &user.SetHumanEmail_SendCode{SendCode: &user.SendEmailVerificationCode{}},
},
},
},
wantErr: false,
},
@@ -2219,7 +2242,97 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
IdToken: gu.Ptr("idToken"),
},
},
IdpId: idpID,
IdpId: oauthIdpID,
UserId: "id",
UserName: "",
RawInformation: func() *structpb.Struct {
s, err := structpb.NewStruct(map[string]interface{}{
"RawInfo": map[string]interface{}{
"id": "id",
"preferred_username": "username",
},
})
require.NoError(t, err)
return s
}(),
},
},
wantErr: false,
},
{
name: "retrieve successful oidc intent",
args: args{
CTX,
&user.RetrieveIdentityProviderIntentRequest{
IdpIntentId: oidcSuccessful,
IdpIntentToken: oidcToken,
},
},
want: &user.RetrieveIdentityProviderIntentResponse{
Details: &object.Details{
ChangeDate: timestamppb.New(oidcChangeDate),
ResourceOwner: Instance.ID(),
Sequence: oidcSequence,
},
UserId: "",
IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Oauth{
Oauth: &user.IDPOAuthAccessInformation{
AccessToken: "accessToken",
IdToken: gu.Ptr("idToken"),
},
},
IdpId: oidcIdpID,
UserId: "id",
UserName: "username",
RawInformation: func() *structpb.Struct {
s, err := structpb.NewStruct(map[string]interface{}{
"sub": "id",
"preferred_username": "username",
})
require.NoError(t, err)
return s
}(),
},
AddHumanUser: &user.AddHumanUserRequest{
Username: gu.Ptr("username"),
Profile: &user.SetHumanProfile{
PreferredLanguage: gu.Ptr("und"),
},
IdpLinks: []*user.IDPLink{
{IdpId: oidcIdpID, UserId: "id", UserName: "username"},
},
Email: &user.SetHumanEmail{
Verification: &user.SetHumanEmail_SendCode{SendCode: &user.SendEmailVerificationCode{}},
},
},
},
wantErr: false,
},
{
name: "retrieve successful oidc intent with linked user",
args: args{
CTX,
&user.RetrieveIdentityProviderIntentRequest{
IdpIntentId: oidcSuccessfulWithUserID,
IdpIntentToken: oidcWithUserIDToken,
},
},
want: &user.RetrieveIdentityProviderIntentResponse{
Details: &object.Details{
ChangeDate: timestamppb.New(oidcWithUserIDChangeDate),
ResourceOwner: Instance.ID(),
Sequence: oidcWithUserIDSequence,
},
UserId: "user",
IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Oauth{
Oauth: &user.IDPOAuthAccessInformation{
AccessToken: "accessToken",
IdToken: gu.Ptr("idToken"),
},
},
IdpId: oidcIdpID,
UserId: "id",
UserName: "username",
RawInformation: func() *structpb.Struct {
@@ -2263,7 +2376,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
}(),
},
},
IdpId: idpID,
IdpId: ldapIdpID,
UserId: "id",
UserName: "username",
RawInformation: func() *structpb.Struct {
@@ -2276,6 +2389,18 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
return s
}(),
},
AddHumanUser: &user.AddHumanUserRequest{
Username: gu.Ptr("username"),
Profile: &user.SetHumanProfile{
PreferredLanguage: gu.Ptr("en"),
},
IdpLinks: []*user.IDPLink{
{IdpId: ldapIdpID, UserId: "id", UserName: "username"},
},
Email: &user.SetHumanEmail{
Verification: &user.SetHumanEmail_SendCode{SendCode: &user.SendEmailVerificationCode{}},
},
},
},
wantErr: false,
},
@@ -2309,7 +2434,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
}(),
},
},
IdpId: idpID,
IdpId: ldapIdpID,
UserId: "id",
UserName: "username",
RawInformation: func() *structpb.Struct {
@@ -2346,7 +2471,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
Assertion: []byte("<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"id\" IssueInstant=\"0001-01-01T00:00:00Z\" Version=\"\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" NameQualifier=\"\" SPNameQualifier=\"\" Format=\"\" SPProvidedID=\"\"></Issuer></Assertion>"),
},
},
IdpId: idpID,
IdpId: samlIdpID,
UserId: "id",
UserName: "",
RawInformation: func() *structpb.Struct {
@@ -2360,6 +2485,56 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
return s
}(),
},
AddHumanUser: &user.AddHumanUserRequest{
Profile: &user.SetHumanProfile{
PreferredLanguage: gu.Ptr("und"),
},
IdpLinks: []*user.IDPLink{
{IdpId: samlIdpID, UserId: "id"},
},
Email: &user.SetHumanEmail{
Verification: &user.SetHumanEmail_SendCode{SendCode: &user.SendEmailVerificationCode{}},
},
},
},
wantErr: false,
},
{
name: "retrieve successful saml intent with linked user",
args: args{
CTX,
&user.RetrieveIdentityProviderIntentRequest{
IdpIntentId: samlSuccessfulWithUserID,
IdpIntentToken: samlWithUserToken,
},
},
want: &user.RetrieveIdentityProviderIntentResponse{
Details: &object.Details{
ChangeDate: timestamppb.New(samlWithUserChangeDate),
ResourceOwner: Instance.ID(),
Sequence: samlWithUserSequence,
},
IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Saml{
Saml: &user.IDPSAMLAccessInformation{
Assertion: []byte("<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"id\" IssueInstant=\"0001-01-01T00:00:00Z\" Version=\"\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" NameQualifier=\"\" SPNameQualifier=\"\" Format=\"\" SPProvidedID=\"\"></Issuer></Assertion>"),
},
},
IdpId: samlIdpID,
UserId: "id",
UserName: "",
RawInformation: func() *structpb.Struct {
s, err := structpb.NewStruct(map[string]interface{}{
"id": "id",
"attributes": map[string]interface{}{
"attribute1": []interface{}{"value1"},
},
})
require.NoError(t, err)
return s
}(),
},
UserId: "user",
},
wantErr: false,
},
@@ -2369,11 +2544,11 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
got, err := Client.RetrieveIdentityProviderIntent(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
return
}
require.NoError(t, err)
grpc.AllFieldsEqual(t, tt.want.ProtoReflect(), got.ProtoReflect(), grpc.CustomMappers)
assert.EqualExportedValues(t, tt.want, got)
})
}
}

View File

@@ -0,0 +1,370 @@
package user
import (
"context"
"encoding/json"
"errors"
oidc_pkg "github.com/zitadel/oidc/v3/pkg/oidc"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/apple"
"github.com/zitadel/zitadel/internal/idp/providers/azuread"
"github.com/zitadel/zitadel/internal/idp/providers/github"
"github.com/zitadel/zitadel/internal/idp/providers/gitlab"
"github.com/zitadel/zitadel/internal/idp/providers/google"
"github.com/zitadel/zitadel/internal/idp/providers/jwt"
"github.com/zitadel/zitadel/internal/idp/providers/ldap"
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
"github.com/zitadel/zitadel/internal/idp/providers/oidc"
"github.com/zitadel/zitadel/internal/idp/providers/saml"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) StartIdentityProviderIntent(ctx context.Context, req *user.StartIdentityProviderIntentRequest) (_ *user.StartIdentityProviderIntentResponse, err error) {
switch t := req.GetContent().(type) {
case *user.StartIdentityProviderIntentRequest_Urls:
return s.startIDPIntent(ctx, req.GetIdpId(), t.Urls)
case *user.StartIdentityProviderIntentRequest_Ldap:
return s.startLDAPIntent(ctx, req.GetIdpId(), t.Ldap)
default:
return nil, zerrors.ThrowUnimplementedf(nil, "USERv2-S2g21", "type oneOf %T in method StartIdentityProviderIntent not implemented", t)
}
}
func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.RedirectURLs) (*user.StartIdentityProviderIntentResponse, error) {
state, session, err := s.command.AuthFromProvider(ctx, idpID, s.idpCallback(ctx), s.samlRootURL(ctx, idpID))
if err != nil {
return nil, err
}
_, details, err := s.command.CreateIntent(ctx, state, idpID, urls.GetSuccessUrl(), urls.GetFailureUrl(), authz.GetInstance(ctx).InstanceID(), session.PersistentParameters())
if err != nil {
return nil, err
}
content, redirect := session.GetAuth(ctx)
if redirect {
return &user.StartIdentityProviderIntentResponse{
Details: object.DomainToDetailsPb(details),
NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: content},
}, nil
}
return &user.StartIdentityProviderIntentResponse{
Details: object.DomainToDetailsPb(details),
NextStep: &user.StartIdentityProviderIntentResponse_PostForm{
PostForm: []byte(content),
},
}, nil
}
func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredentials *user.LDAPCredentials) (*user.StartIdentityProviderIntentResponse, error) {
intentWriteModel, details, err := s.command.CreateIntent(ctx, "", idpID, "", "", authz.GetInstance(ctx).InstanceID(), nil)
if err != nil {
return nil, err
}
externalUser, userID, attributes, err := s.ldapLogin(ctx, intentWriteModel.IDPID, ldapCredentials.GetUsername(), ldapCredentials.GetPassword())
if err != nil {
if err := s.command.FailIDPIntent(ctx, intentWriteModel, err.Error()); err != nil {
return nil, err
}
return nil, err
}
token, err := s.command.SucceedLDAPIDPIntent(ctx, intentWriteModel, externalUser, userID, attributes)
if err != nil {
return nil, err
}
return &user.StartIdentityProviderIntentResponse{
Details: object.DomainToDetailsPb(details),
NextStep: &user.StartIdentityProviderIntentResponse_IdpIntent{
IdpIntent: &user.IDPIntent{
IdpIntentId: intentWriteModel.AggregateID,
IdpIntentToken: token,
UserId: userID,
},
},
}, nil
}
func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUserID string) (string, error) {
idQuery, err := query.NewIDPUserLinkIDPIDSearchQuery(idpID)
if err != nil {
return "", err
}
externalIDQuery, err := query.NewIDPUserLinksExternalIDSearchQuery(externalUserID)
if err != nil {
return "", err
}
queries := []query.SearchQuery{
idQuery, externalIDQuery,
}
links, err := s.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: queries}, nil)
if err != nil {
return "", err
}
if len(links.Links) == 1 {
return links.Links[0].UserID, nil
}
return "", nil
}
func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, map[string][]string, error) {
provider, err := s.command.GetProvider(ctx, idpID, "", "")
if err != nil {
return nil, "", nil, err
}
ldapProvider, ok := provider.(*ldap.Provider)
if !ok {
return nil, "", nil, zerrors.ThrowInvalidArgument(nil, "IDP-9a02j2n2bh", "Errors.ExternalIDP.IDPTypeNotImplemented")
}
session := ldapProvider.GetSession(username, password)
externalUser, err := session.FetchUser(ctx)
if errors.Is(err, ldap.ErrFailedLogin) || errors.Is(err, ldap.ErrNoSingleUser) {
return nil, "", nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-nzun2i", "Errors.User.ExternalIDP.LoginFailed")
}
if err != nil {
return nil, "", nil, err
}
userID, err := s.checkLinkedExternalUser(ctx, idpID, externalUser.GetID())
if err != nil {
return nil, "", nil, err
}
attributes := make(map[string][]string, 0)
for _, item := range session.Entry.Attributes {
attributes[item.Name] = item.Values
}
return externalUser, userID, attributes, nil
}
func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.RetrieveIdentityProviderIntentRequest) (_ *user.RetrieveIdentityProviderIntentResponse, err error) {
intent, err := s.command.GetIntentWriteModel(ctx, req.GetIdpIntentId(), "")
if err != nil {
return nil, err
}
if err := s.checkIntentToken(req.GetIdpIntentToken(), intent.AggregateID); err != nil {
return nil, err
}
if intent.State != domain.IDPIntentStateSucceeded {
return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-nme4gszsvx", "Errors.Intent.NotSucceeded")
}
idpIntent, err := idpIntentToIDPIntentPb(intent, s.idpAlg)
if err != nil {
return nil, err
}
if idpIntent.UserId == "" {
provider, err := s.command.GetProvider(ctx, idpIntent.IdpInformation.IdpId, "", "")
if err != nil && !errors.Is(err, oidc_pkg.ErrDiscoveryFailed) {
return nil, err
}
var idpUser idp.User
switch p := provider.(type) {
case *apple.Provider:
idpUser, err = unmarshalIdpUser(intent.IDPUser, &apple.User{})
case *oauth.Provider:
idpUser, err = unmarshalRawIdpUser(intent.IDPUser, p.User())
case *oidc.Provider:
idpUser, err = unmarshalIdpUser(intent.IDPUser, &oidc.User{UserInfo: &oidc_pkg.UserInfo{}})
case *jwt.Provider:
idpUser, err = unmarshalIdpUser(intent.IDPUser, &jwt.User{})
case *azuread.Provider:
idpUser, err = unmarshalRawIdpUser(intent.IDPUser, p.User())
case *github.Provider:
idpUser, err = unmarshalIdpUser(intent.IDPUser, &github.User{})
case *gitlab.Provider:
idpUser, err = unmarshalIdpUser(intent.IDPUser, &oidc.User{UserInfo: &oidc_pkg.UserInfo{}})
case *google.Provider:
idpUser, err = unmarshalIdpUser(intent.IDPUser, &oidc.User{UserInfo: &oidc_pkg.UserInfo{}})
case *saml.Provider:
idpUser, err = unmarshalIdpUser(intent.IDPUser, &saml.UserMapper{})
case *ldap.Provider:
idpUser, err = unmarshalIdpUser(intent.IDPUser, &ldap.User{})
default:
return nil, zerrors.ThrowInvalidArgument(nil, "IDP-7rPBbls4Zn", "Errors.ExternalIDP.IDPTypeNotImplemented")
}
if err != nil {
return nil, err
}
idpIntent.AddHumanUser = idpUserToAddHumanUser(idpUser, idpIntent.IdpInformation.IdpId)
}
return idpIntent, nil
}
type rawUserMapper struct {
RawInfo map[string]interface{}
}
func unmarshalRawIdpUser(idpUserData []byte, idpUser idp.User) (idp.User, error) {
userMapper := &rawUserMapper{}
if err := json.Unmarshal(idpUserData, userMapper); err != nil {
return nil, err
}
idpUserData, err := json.Marshal(userMapper.RawInfo)
if err != nil {
return nil, err
}
return unmarshalIdpUser(idpUserData, idpUser)
}
func unmarshalIdpUser(idpUserData []byte, idpUser idp.User) (idp.User, error) {
if err := json.Unmarshal(idpUserData, idpUser); err != nil {
return nil, err
}
return idpUser, nil
}
func idpIntentToIDPIntentPb(intent *command.IDPIntentWriteModel, alg crypto.EncryptionAlgorithm) (_ *user.RetrieveIdentityProviderIntentResponse, err error) {
rawInformation := new(structpb.Struct)
err = rawInformation.UnmarshalJSON(intent.IDPUser)
if err != nil {
return nil, err
}
information := &user.RetrieveIdentityProviderIntentResponse{
IdpInformation: &user.IDPInformation{
IdpId: intent.IDPID,
UserId: intent.IDPUserID,
UserName: intent.IDPUserName,
RawInformation: rawInformation,
},
UserId: intent.UserID,
}
information.Details = intentToDetailsPb(intent)
// OAuth / OIDC
if intent.IDPIDToken != "" || intent.IDPAccessToken != nil {
information.IdpInformation.Access, err = idpOAuthTokensToPb(intent.IDPIDToken, intent.IDPAccessToken, alg)
if err != nil {
return nil, err
}
}
// LDAP
if intent.IDPEntryAttributes != nil {
access, err := IDPEntryAttributesToPb(intent.IDPEntryAttributes)
if err != nil {
return nil, err
}
information.IdpInformation.Access = access
}
// SAML
if intent.Assertion != nil {
assertion, err := crypto.Decrypt(intent.Assertion, alg)
if err != nil {
return nil, err
}
information.IdpInformation.Access = IDPSAMLResponseToPb(assertion)
}
return information, nil
}
func idpOAuthTokensToPb(idpIDToken string, idpAccessToken *crypto.CryptoValue, alg crypto.EncryptionAlgorithm) (_ *user.IDPInformation_Oauth, err error) {
var idToken *string
if idpIDToken != "" {
idToken = &idpIDToken
}
var accessToken string
if idpAccessToken != nil {
accessToken, err = crypto.DecryptString(idpAccessToken, alg)
if err != nil {
return nil, err
}
}
return &user.IDPInformation_Oauth{
Oauth: &user.IDPOAuthAccessInformation{
AccessToken: accessToken,
IdToken: idToken,
},
}, nil
}
func intentToDetailsPb(intent *command.IDPIntentWriteModel) *object_pb.Details {
return &object_pb.Details{
Sequence: intent.ProcessedSequence,
ChangeDate: timestamppb.New(intent.ChangeDate),
ResourceOwner: intent.ResourceOwner,
}
}
func IDPEntryAttributesToPb(entryAttributes map[string][]string) (*user.IDPInformation_Ldap, error) {
values := make(map[string]interface{}, 0)
for k, v := range entryAttributes {
intValues := make([]interface{}, len(v))
for i, value := range v {
intValues[i] = value
}
values[k] = intValues
}
attributes, err := structpb.NewStruct(values)
if err != nil {
return nil, err
}
return &user.IDPInformation_Ldap{
Ldap: &user.IDPLDAPAccessInformation{
Attributes: attributes,
},
}, nil
}
func IDPSAMLResponseToPb(assertion []byte) *user.IDPInformation_Saml {
return &user.IDPInformation_Saml{
Saml: &user.IDPSAMLAccessInformation{
Assertion: assertion,
},
}
}
func (s *Server) checkIntentToken(token string, intentID string) error {
return crypto.CheckToken(s.idpAlg, token, intentID)
}
func idpUserToAddHumanUser(idpUser idp.User, idpID string) *user.AddHumanUserRequest {
addHumanUser := &user.AddHumanUserRequest{
Profile: &user.SetHumanProfile{
GivenName: idpUser.GetFirstName(),
FamilyName: idpUser.GetLastName(),
},
Email: &user.SetHumanEmail{
Email: string(idpUser.GetEmail()),
Verification: &user.SetHumanEmail_SendCode{},
},
Metadata: make([]*user.SetMetadataEntry, 0),
IdpLinks: []*user.IDPLink{
{
IdpId: idpID,
UserId: idpUser.GetID(),
UserName: idpUser.GetPreferredUsername(),
},
},
}
if username := idpUser.GetPreferredUsername(); username != "" {
addHumanUser.Username = &username
}
if nickName := idpUser.GetNickname(); nickName != "" {
addHumanUser.Profile.NickName = &nickName
}
if displayName := idpUser.GetDisplayName(); displayName != "" {
addHumanUser.Profile.DisplayName = &displayName
}
if lang := idpUser.GetPreferredLanguage().String(); lang != "" {
addHumanUser.Profile.PreferredLanguage = &lang
}
if isEmailVerified := idpUser.IsEmailVerified(); isEmailVerified {
addHumanUser.Email.Verification = &user.SetHumanEmail_IsVerified{IsVerified: isEmailVerified}
}
if phone := idpUser.GetPhone(); phone != "" {
addHumanUser.Phone = &user.SetHumanPhone{
Phone: string(phone),
Verification: &user.SetHumanPhone_SendCode{},
}
if isPhoneVerified := idpUser.IsPhoneVerified(); isPhoneVerified {
addHumanUser.Phone.Verification = &user.SetHumanPhone_IsVerified{IsVerified: isPhoneVerified}
}
}
return addHumanUser
}

View File

@@ -2,28 +2,19 @@ package user
import (
"context"
"errors"
"io"
"golang.org/x/text/language"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/ldap"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest) (_ *user.AddHumanUserResponse, err error) {
human, err := AddUserRequestToAddHuman(req)
if err != nil {
return nil, err
@@ -356,236 +347,6 @@ func userGrantsToIDs(userGrants []*query.UserGrant) []string {
return converted
}
func (s *Server) StartIdentityProviderIntent(ctx context.Context, req *user.StartIdentityProviderIntentRequest) (_ *user.StartIdentityProviderIntentResponse, err error) {
switch t := req.GetContent().(type) {
case *user.StartIdentityProviderIntentRequest_Urls:
return s.startIDPIntent(ctx, req.GetIdpId(), t.Urls)
case *user.StartIdentityProviderIntentRequest_Ldap:
return s.startLDAPIntent(ctx, req.GetIdpId(), t.Ldap)
default:
return nil, zerrors.ThrowUnimplementedf(nil, "USERv2-S2g21", "type oneOf %T in method StartIdentityProviderIntent not implemented", t)
}
}
func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.RedirectURLs) (*user.StartIdentityProviderIntentResponse, error) {
state, session, err := s.command.AuthFromProvider(ctx, idpID, s.idpCallback(ctx), s.samlRootURL(ctx, idpID))
if err != nil {
return nil, err
}
_, details, err := s.command.CreateIntent(ctx, state, idpID, urls.GetSuccessUrl(), urls.GetFailureUrl(), authz.GetInstance(ctx).InstanceID(), session.PersistentParameters())
if err != nil {
return nil, err
}
content, redirect := session.GetAuth(ctx)
if redirect {
return &user.StartIdentityProviderIntentResponse{
Details: object.DomainToDetailsPb(details),
NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: content},
}, nil
}
return &user.StartIdentityProviderIntentResponse{
Details: object.DomainToDetailsPb(details),
NextStep: &user.StartIdentityProviderIntentResponse_PostForm{
PostForm: []byte(content),
},
}, nil
}
func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredentials *user.LDAPCredentials) (*user.StartIdentityProviderIntentResponse, error) {
intentWriteModel, details, err := s.command.CreateIntent(ctx, "", idpID, "", "", authz.GetInstance(ctx).InstanceID(), nil)
if err != nil {
return nil, err
}
externalUser, userID, attributes, err := s.ldapLogin(ctx, intentWriteModel.IDPID, ldapCredentials.GetUsername(), ldapCredentials.GetPassword())
if err != nil {
if err := s.command.FailIDPIntent(ctx, intentWriteModel, err.Error()); err != nil {
return nil, err
}
return nil, err
}
token, err := s.command.SucceedLDAPIDPIntent(ctx, intentWriteModel, externalUser, userID, attributes)
if err != nil {
return nil, err
}
return &user.StartIdentityProviderIntentResponse{
Details: object.DomainToDetailsPb(details),
NextStep: &user.StartIdentityProviderIntentResponse_IdpIntent{
IdpIntent: &user.IDPIntent{
IdpIntentId: intentWriteModel.AggregateID,
IdpIntentToken: token,
UserId: userID,
},
},
}, nil
}
func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUserID string) (string, error) {
idQuery, err := query.NewIDPUserLinkIDPIDSearchQuery(idpID)
if err != nil {
return "", err
}
externalIDQuery, err := query.NewIDPUserLinksExternalIDSearchQuery(externalUserID)
if err != nil {
return "", err
}
queries := []query.SearchQuery{
idQuery, externalIDQuery,
}
links, err := s.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: queries}, nil)
if err != nil {
return "", err
}
if len(links.Links) == 1 {
return links.Links[0].UserID, nil
}
return "", nil
}
func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, map[string][]string, error) {
provider, err := s.command.GetProvider(ctx, idpID, "", "")
if err != nil {
return nil, "", nil, err
}
ldapProvider, ok := provider.(*ldap.Provider)
if !ok {
return nil, "", nil, zerrors.ThrowInvalidArgument(nil, "IDP-9a02j2n2bh", "Errors.ExternalIDP.IDPTypeNotImplemented")
}
session := ldapProvider.GetSession(username, password)
externalUser, err := session.FetchUser(ctx)
if errors.Is(err, ldap.ErrFailedLogin) || errors.Is(err, ldap.ErrNoSingleUser) {
return nil, "", nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-nzun2i", "Errors.User.ExternalIDP.LoginFailed")
}
if err != nil {
return nil, "", nil, err
}
userID, err := s.checkLinkedExternalUser(ctx, idpID, externalUser.GetID())
if err != nil {
return nil, "", nil, err
}
attributes := make(map[string][]string, 0)
for _, item := range session.Entry.Attributes {
attributes[item.Name] = item.Values
}
return externalUser, userID, attributes, nil
}
func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.RetrieveIdentityProviderIntentRequest) (_ *user.RetrieveIdentityProviderIntentResponse, err error) {
intent, err := s.command.GetIntentWriteModel(ctx, req.GetIdpIntentId(), "")
if err != nil {
return nil, err
}
if err := s.checkIntentToken(req.GetIdpIntentToken(), intent.AggregateID); err != nil {
return nil, err
}
if intent.State != domain.IDPIntentStateSucceeded {
return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-nme4gszsvx", "Errors.Intent.NotSucceeded")
}
return idpIntentToIDPIntentPb(intent, s.idpAlg)
}
func idpIntentToIDPIntentPb(intent *command.IDPIntentWriteModel, alg crypto.EncryptionAlgorithm) (_ *user.RetrieveIdentityProviderIntentResponse, err error) {
rawInformation := new(structpb.Struct)
err = rawInformation.UnmarshalJSON(intent.IDPUser)
if err != nil {
return nil, err
}
information := &user.RetrieveIdentityProviderIntentResponse{
Details: intentToDetailsPb(intent),
IdpInformation: &user.IDPInformation{
IdpId: intent.IDPID,
UserId: intent.IDPUserID,
UserName: intent.IDPUserName,
RawInformation: rawInformation,
},
UserId: intent.UserID,
}
if intent.IDPIDToken != "" || intent.IDPAccessToken != nil {
information.IdpInformation.Access, err = idpOAuthTokensToPb(intent.IDPIDToken, intent.IDPAccessToken, alg)
if err != nil {
return nil, err
}
}
if intent.IDPEntryAttributes != nil {
access, err := IDPEntryAttributesToPb(intent.IDPEntryAttributes)
if err != nil {
return nil, err
}
information.IdpInformation.Access = access
}
if intent.Assertion != nil {
assertion, err := crypto.Decrypt(intent.Assertion, alg)
if err != nil {
return nil, err
}
information.IdpInformation.Access = IDPSAMLResponseToPb(assertion)
}
return information, nil
}
func idpOAuthTokensToPb(idpIDToken string, idpAccessToken *crypto.CryptoValue, alg crypto.EncryptionAlgorithm) (_ *user.IDPInformation_Oauth, err error) {
var idToken *string
if idpIDToken != "" {
idToken = &idpIDToken
}
var accessToken string
if idpAccessToken != nil {
accessToken, err = crypto.DecryptString(idpAccessToken, alg)
if err != nil {
return nil, err
}
}
return &user.IDPInformation_Oauth{
Oauth: &user.IDPOAuthAccessInformation{
AccessToken: accessToken,
IdToken: idToken,
},
}, nil
}
func intentToDetailsPb(intent *command.IDPIntentWriteModel) *object_pb.Details {
return &object_pb.Details{
Sequence: intent.ProcessedSequence,
ChangeDate: timestamppb.New(intent.ChangeDate),
ResourceOwner: intent.ResourceOwner,
}
}
func IDPEntryAttributesToPb(entryAttributes map[string][]string) (*user.IDPInformation_Ldap, error) {
values := make(map[string]interface{}, 0)
for k, v := range entryAttributes {
intValues := make([]interface{}, len(v))
for i, value := range v {
intValues[i] = value
}
values[k] = intValues
}
attributes, err := structpb.NewStruct(values)
if err != nil {
return nil, err
}
return &user.IDPInformation_Ldap{
Ldap: &user.IDPLDAPAccessInformation{
Attributes: attributes,
},
}, nil
}
func IDPSAMLResponseToPb(assertion []byte) *user.IDPInformation_Saml {
return &user.IDPInformation_Saml{
Saml: &user.IDPSAMLAccessInformation{
Assertion: assertion,
},
}
}
func (s *Server) checkIntentToken(token string, intentID string) error {
return crypto.CheckToken(s.idpAlg, token, intentID)
}
func (s *Server) ListAuthenticationMethodTypes(ctx context.Context, req *user.ListAuthenticationMethodTypesRequest) (*user.ListAuthenticationMethodTypesResponse, error) {
authMethods, err := s.query.ListUserAuthMethodTypes(ctx, req.GetUserId(), true, req.GetDomainQuery().GetIncludeWithoutDomain(), req.GetDomainQuery().GetDomain())
if err != nil {

View File

@@ -11,7 +11,6 @@ import (
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/grpc"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
@@ -322,7 +321,7 @@ func Test_idpIntentToIDPIntentPb(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
got, err := idpIntentToIDPIntentPb(tt.args.intent, tt.args.alg)
require.ErrorIs(t, err, tt.res.err)
grpc.AllFieldsEqual(t, tt.res.resp.ProtoReflect(), got.ProtoReflect(), grpc.CustomMappers)
assert.EqualExportedValues(t, tt.res.resp, got)
})
}
}

View File

@@ -2146,17 +2146,29 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
}
func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
idpID := Instance.AddGenericOAuthProvider(IamCTX, gofakeit.AppName()).GetId()
intentID := Instance.CreateIntent(CTX, idpID).GetIdpIntent().GetIdpIntentId()
successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", "")
oauthIdpID := Instance.AddGenericOAuthProvider(IamCTX, gofakeit.AppName()).GetId()
oidcIdpID := Instance.AddGenericOIDCProvider(IamCTX, gofakeit.AppName()).GetId()
samlIdpID := Instance.AddSAMLPostProvider(IamCTX)
ldapIdpID := Instance.AddLDAPProvider(IamCTX)
authURL, err := url.Parse(Instance.CreateIntent(CTX, oauthIdpID).GetAuthUrl())
require.NoError(t, err)
successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", "user")
intentID := authURL.Query().Get("state")
successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "")
require.NoError(t, err)
ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), idpID, "id", "")
successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user")
require.NoError(t, err)
ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), idpID, "id", "user")
oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "")
require.NoError(t, err)
samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), idpID, "id", "")
oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user")
require.NoError(t, err)
ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "")
require.NoError(t, err)
ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "user")
require.NoError(t, err)
samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "")
require.NoError(t, err)
samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user")
require.NoError(t, err)
type args struct {
ctx context.Context
@@ -2191,7 +2203,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
wantErr: true,
},
{
name: "retrieve successful intent",
name: "retrieve successful oauth intent",
args: args{
CTX,
&user.RetrieveIdentityProviderIntentRequest{
@@ -2212,13 +2224,15 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
IdToken: gu.Ptr("idToken"),
},
},
IdpId: idpID,
IdpId: oauthIdpID,
UserId: "id",
UserName: "username",
UserName: "",
RawInformation: func() *structpb.Struct {
s, err := structpb.NewStruct(map[string]interface{}{
"sub": "id",
"preferred_username": "username",
"RawInfo": map[string]interface{}{
"id": "id",
"preferred_username": "username",
},
})
require.NoError(t, err)
return s
@@ -2250,7 +2264,85 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
IdToken: gu.Ptr("idToken"),
},
},
IdpId: idpID,
IdpId: oauthIdpID,
UserId: "id",
UserName: "",
RawInformation: func() *structpb.Struct {
s, err := structpb.NewStruct(map[string]interface{}{
"RawInfo": map[string]interface{}{
"id": "id",
"preferred_username": "username",
},
})
require.NoError(t, err)
return s
}(),
},
},
wantErr: false,
},
{
name: "retrieve successful oidc intent",
args: args{
CTX,
&user.RetrieveIdentityProviderIntentRequest{
IdpIntentId: oidcSuccessful,
IdpIntentToken: oidcToken,
},
},
want: &user.RetrieveIdentityProviderIntentResponse{
Details: &object.Details{
ChangeDate: timestamppb.New(oidcChangeDate),
ResourceOwner: Instance.ID(),
Sequence: oidcSequence,
},
UserId: "",
IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Oauth{
Oauth: &user.IDPOAuthAccessInformation{
AccessToken: "accessToken",
IdToken: gu.Ptr("idToken"),
},
},
IdpId: oidcIdpID,
UserId: "id",
UserName: "username",
RawInformation: func() *structpb.Struct {
s, err := structpb.NewStruct(map[string]interface{}{
"sub": "id",
"preferred_username": "username",
})
require.NoError(t, err)
return s
}(),
},
},
wantErr: false,
},
{
name: "retrieve successful oidc intent with linked user",
args: args{
CTX,
&user.RetrieveIdentityProviderIntentRequest{
IdpIntentId: oidcSuccessfulWithUserID,
IdpIntentToken: oidcWithUserIDToken,
},
},
want: &user.RetrieveIdentityProviderIntentResponse{
Details: &object.Details{
ChangeDate: timestamppb.New(oidcWithUserIDChangeDate),
ResourceOwner: Instance.ID(),
Sequence: oidcWithUserIDSequence,
},
UserId: "user",
IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Oauth{
Oauth: &user.IDPOAuthAccessInformation{
AccessToken: "accessToken",
IdToken: gu.Ptr("idToken"),
},
},
IdpId: oidcIdpID,
UserId: "id",
UserName: "username",
RawInformation: func() *structpb.Struct {
@@ -2294,7 +2386,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
}(),
},
},
IdpId: idpID,
IdpId: ldapIdpID,
UserId: "id",
UserName: "username",
RawInformation: func() *structpb.Struct {
@@ -2340,7 +2432,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
}(),
},
},
IdpId: idpID,
IdpId: ldapIdpID,
UserId: "id",
UserName: "username",
RawInformation: func() *structpb.Struct {
@@ -2377,7 +2469,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
Assertion: []byte("<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"id\" IssueInstant=\"0001-01-01T00:00:00Z\" Version=\"\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" NameQualifier=\"\" SPNameQualifier=\"\" Format=\"\" SPProvidedID=\"\"></Issuer></Assertion>"),
},
},
IdpId: idpID,
IdpId: samlIdpID,
UserId: "id",
UserName: "",
RawInformation: func() *structpb.Struct {
@@ -2394,6 +2486,45 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
},
wantErr: false,
},
{
name: "retrieve successful saml intent with linked user",
args: args{
CTX,
&user.RetrieveIdentityProviderIntentRequest{
IdpIntentId: samlSuccessfulWithUserID,
IdpIntentToken: samlWithUserToken,
},
},
want: &user.RetrieveIdentityProviderIntentResponse{
Details: &object.Details{
ChangeDate: timestamppb.New(samlWithUserChangeDate),
ResourceOwner: Instance.ID(),
Sequence: samlWithUserSequence,
},
IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Saml{
Saml: &user.IDPSAMLAccessInformation{
Assertion: []byte("<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"id\" IssueInstant=\"0001-01-01T00:00:00Z\" Version=\"\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" NameQualifier=\"\" SPNameQualifier=\"\" Format=\"\" SPProvidedID=\"\"></Issuer></Assertion>"),
},
},
IdpId: samlIdpID,
UserId: "id",
UserName: "",
RawInformation: func() *structpb.Struct {
s, err := structpb.NewStruct(map[string]interface{}{
"id": "id",
"attributes": map[string]interface{}{
"attribute1": []interface{}{"value1"},
},
})
require.NoError(t, err)
return s
}(),
},
UserId: "user",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {