Files
zitadel/internal/api/grpc/user/v2/integration_test/intent_test.go
T
d08779e279 feat: supporting metadata updates in actions v2 via RetrieveIdentityProviderIntent API (#11810)
# Which Problems Are Solved

Adds support for returning CreateUser/UpdateUser action payloads from
RetrieveIdentityProviderIntent so Actions v2 can update user metadata
(and other fields) using the v2 user APIs, ensuring parity with user
creation using Actions v2.

# How the Problems Are Solved

By:
- deprecating `AddHumanUser` and `UpdateHumanUser` fields in
`RetrieveIdentityProviderIntentResponse`
- adding a oneof field called `UserAction` with `CreateUser` and
`UpdateUser` fields to support user creation/update
- setting `UserAction` in the `RetrieveIdentityProviderIntentResponse`
for user creation/update

# Additional Changes
N/A

# Additional Context
- Related to https://github.com/zitadel/zitadel/issues/11369
- Follow-up for PRs https://github.com/zitadel/zitadel/pull/11719,
https://github.com/zitadel/zitadel/pull/11747
- Actions V2 example docs will be updated in a follow-up PR

---------

Co-authored-by: Copilot <[email protected]>
2026-03-12 16:01:58 +00:00

1344 lines
41 KiB
Go

//go:build integration
package user_test
import (
"context"
"fmt"
"net/url"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/internal/integration/sink"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func TestServer_StartIdentityProviderIntent(t *testing.T) {
idpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id)
orgIdpResp := Instance.AddOrgGenericOAuthProvider(OrgCTX, Instance.DefaultOrg.Id)
orgResp := Instance.CreateOrganization(IamCTX, integration.OrganizationName(), integration.Email())
notDefaultOrgIdpResp := Instance.AddOrgGenericOAuthProvider(IamCTX, orgResp.OrganizationId)
samlIdpID := Instance.AddSAMLProvider(IamCTX)
samlRedirectIdpID := Instance.AddSAMLRedirectProvider(IamCTX, "")
samlPostIdpID := Instance.AddSAMLPostProvider(IamCTX)
jwtIdPID := Instance.AddJWTProvider(IamCTX)
type args struct {
ctx context.Context
req *user.StartIdentityProviderIntentRequest
}
type want struct {
details *object.Details
url string
parametersExisting []string
parametersEqual map[string]string
postForm bool
}
tests := []struct {
name string
args args
want want
wantErr bool
}{
{
name: "missing urls",
args: args{
OrgCTX,
&user.StartIdentityProviderIntentRequest{
IdpId: idpResp.Id,
},
},
wantErr: true,
},
{
name: "next step oauth auth url",
args: args{
OrgCTX,
&user.StartIdentityProviderIntentRequest{
IdpId: idpResp.Id,
Content: &user.StartIdentityProviderIntentRequest_Urls{
Urls: &user.RedirectURLs{
SuccessUrl: "https://example.com/success",
FailureUrl: "https://example.com/failure",
},
},
},
},
want: want{
details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
url: "https://example.com/oauth/v2/authorize",
parametersEqual: map[string]string{
"client_id": "clientID",
"prompt": "select_account",
"redirect_uri": "http://" + Instance.Domain + ":8082/idps/callback",
"response_type": "code",
"scope": "openid profile email",
},
parametersExisting: []string{"state"},
},
wantErr: false,
},
{
name: "next step oauth auth url, default org",
args: args{
OrgCTX,
&user.StartIdentityProviderIntentRequest{
IdpId: orgIdpResp.Id,
Content: &user.StartIdentityProviderIntentRequest_Urls{
Urls: &user.RedirectURLs{
SuccessUrl: "https://example.com/success",
FailureUrl: "https://example.com/failure",
},
},
},
},
want: want{
details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
url: "https://example.com/oauth/v2/authorize",
parametersEqual: map[string]string{
"client_id": "clientID",
"prompt": "select_account",
"redirect_uri": "http://" + Instance.Domain + ":8082/idps/callback",
"response_type": "code",
"scope": "openid profile email",
},
parametersExisting: []string{"state"},
},
wantErr: false,
},
{
name: "next step oauth auth url, default org",
args: args{
OrgCTX,
&user.StartIdentityProviderIntentRequest{
IdpId: notDefaultOrgIdpResp.Id,
Content: &user.StartIdentityProviderIntentRequest_Urls{
Urls: &user.RedirectURLs{
SuccessUrl: "https://example.com/success",
FailureUrl: "https://example.com/failure",
},
},
},
},
want: want{
details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
url: "https://example.com/oauth/v2/authorize",
parametersEqual: map[string]string{
"client_id": "clientID",
"prompt": "select_account",
"redirect_uri": "http://" + Instance.Domain + ":8082/idps/callback",
"response_type": "code",
"scope": "openid profile email",
},
parametersExisting: []string{"state"},
},
wantErr: false,
},
{
name: "next step oauth auth url org",
args: args{
OrgCTX,
&user.StartIdentityProviderIntentRequest{
IdpId: orgIdpResp.Id,
Content: &user.StartIdentityProviderIntentRequest_Urls{
Urls: &user.RedirectURLs{
SuccessUrl: "https://example.com/success",
FailureUrl: "https://example.com/failure",
},
},
},
},
want: want{
details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
url: "https://example.com/oauth/v2/authorize",
parametersEqual: map[string]string{
"client_id": "clientID",
"prompt": "select_account",
"redirect_uri": "http://" + Instance.Domain + ":8082/idps/callback",
"response_type": "code",
"scope": "openid profile email",
},
parametersExisting: []string{"state"},
},
wantErr: false,
},
{
name: "next step saml default",
args: args{
OrgCTX,
&user.StartIdentityProviderIntentRequest{
IdpId: samlIdpID,
Content: &user.StartIdentityProviderIntentRequest_Urls{
Urls: &user.RedirectURLs{
SuccessUrl: "https://example.com/success",
FailureUrl: "https://example.com/failure",
},
},
},
},
want: want{
details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
url: "http://localhost:8000/sso",
parametersExisting: []string{"RelayState", "SAMLRequest"},
},
wantErr: false,
},
{
name: "next step saml auth url",
args: args{
OrgCTX,
&user.StartIdentityProviderIntentRequest{
IdpId: samlRedirectIdpID,
Content: &user.StartIdentityProviderIntentRequest_Urls{
Urls: &user.RedirectURLs{
SuccessUrl: "https://example.com/success",
FailureUrl: "https://example.com/failure",
},
},
},
},
want: want{
details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
url: "http://localhost:8000/sso",
parametersExisting: []string{"RelayState", "SAMLRequest"},
},
wantErr: false,
},
{
name: "next step saml form",
args: args{
OrgCTX,
&user.StartIdentityProviderIntentRequest{
IdpId: samlPostIdpID,
Content: &user.StartIdentityProviderIntentRequest_Urls{
Urls: &user.RedirectURLs{
SuccessUrl: "https://example.com/success",
FailureUrl: "https://example.com/failure",
},
},
},
},
want: want{
details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
url: "http://localhost:8000/sso",
parametersExisting: []string{"RelayState", "SAMLRequest"},
postForm: true,
},
wantErr: false,
},
{
name: "next step jwt idp",
args: args{
OrgCTX,
&user.StartIdentityProviderIntentRequest{
IdpId: jwtIdPID,
Content: &user.StartIdentityProviderIntentRequest_Urls{
Urls: &user.RedirectURLs{
SuccessUrl: "https://example.com/success",
FailureUrl: "https://example.com/failure",
},
},
},
},
want: want{
details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
url: "https://example.com/jwt",
parametersExisting: []string{"authRequestID", "userAgentID"},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.StartIdentityProviderIntent(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
if tt.want.url != "" && !tt.want.postForm {
authUrl, err := url.Parse(got.GetAuthUrl())
require.NoError(t, err)
assert.Equal(t, tt.want.url, authUrl.Scheme+"://"+authUrl.Host+authUrl.Path)
require.Len(t, authUrl.Query(), len(tt.want.parametersEqual)+len(tt.want.parametersExisting))
for _, existing := range tt.want.parametersExisting {
assert.True(t, authUrl.Query().Has(existing))
}
for key, equal := range tt.want.parametersEqual {
assert.Equal(t, equal, authUrl.Query().Get(key))
}
}
if tt.want.postForm {
assert.Equal(t, tt.want.url, got.GetFormData().GetUrl())
require.Len(t, got.GetFormData().GetFields(), len(tt.want.parametersEqual)+len(tt.want.parametersExisting))
for _, existing := range tt.want.parametersExisting {
assert.Contains(t, got.GetFormData().GetFields(), existing)
}
for key, equal := range tt.want.parametersEqual {
assert.Equal(t, got.GetFormData().GetFields()[key], equal)
}
}
integration.AssertDetails(t, &user.StartIdentityProviderIntentResponse{
Details: tt.want.details,
}, got)
})
}
}
func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
oauthIdpID := Instance.AddGenericOAuthProvider(IamCTX, integration.IDPName()).GetId()
azureIdpID := Instance.AddAzureADProvider(IamCTX, integration.IDPName()).GetId()
oidcIdpID := Instance.AddGenericOIDCProvider(IamCTX, integration.IDPName()).GetId()
samlIdpID := Instance.AddSAMLPostProvider(IamCTX)
ldapIdpID := Instance.AddLDAPProvider(IamCTX)
jwtIdPID := Instance.AddJWTProvider(IamCTX)
authURL, err := url.Parse(Instance.CreateIntent(OrgCTX, oauthIdpID).GetAuthUrl())
require.NoError(t, err)
intentID := authURL.Query().Get("state")
expiry := time.Now().Add(1 * time.Hour)
expiryFormatted := expiry.Round(time.Millisecond).UTC().Format("2006-01-02T15:04:05.999Z07:00")
intentUser := Instance.CreateHumanUser(IamCTX)
_, err = Instance.CreateUserIDPlink(IamCTX, intentUser.GetUserId(), "idpUserID", oauthIdpID, "username")
require.NoError(t, err)
successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "", expiry)
require.NoError(t, err)
successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", expiry)
require.NoError(t, err)
successfulExpiredID, expiredToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", time.Now().Add(time.Second))
require.NoError(t, err)
// make sure the intent is expired
time.Sleep(2 * time.Second)
successfulConsumedID, consumedToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "idpUserID", intentUser.GetUserId(), expiry)
require.NoError(t, err)
// make sure the intent is consumed
Instance.CreateIntentSession(t, IamCTX, intentUser.GetUserId(), successfulConsumedID, consumedToken)
azureADSuccessful, azureADToken, azureADChangeDate, azureADSequence, err := sink.SuccessfulAzureADIntent(Instance.ID(), azureIdpID, "id", "", expiry)
require.NoError(t, err)
azureADSuccessfulWithUserID, azureADWithUserIDToken, azureADWithUserIDChangeDate, azureADWithUserIDSequence, err := sink.SuccessfulAzureADIntent(Instance.ID(), azureIdpID, "id", "user", expiry)
require.NoError(t, err)
oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "", expiry)
require.NoError(t, err)
oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user", expiry)
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", "", "", expiry)
require.NoError(t, err)
samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user", "", expiry)
require.NoError(t, err)
jwtSuccessfulID, jwtToken, jwtChangeDate, jwtSequence, err := sink.SuccessfulJWTIntent(Instance.ID(), jwtIdPID, "id", "", expiry)
require.NoError(t, err)
jwtSuccessfulWithUserID, jwtWithUserToken, jwtWithUserChangeDate, jwtWithUserSequence, err := sink.SuccessfulJWTIntent(Instance.ID(), jwtIdPID, "id", "user", expiry)
require.NoError(t, err)
type args struct {
ctx context.Context
req *user.RetrieveIdentityProviderIntentRequest
}
tests := []struct {
name string
args args
want *user.RetrieveIdentityProviderIntentResponse
wantErr bool
}{
{
name: "failed intent",
args: args{
OrgCTX,
&user.RetrieveIdentityProviderIntentRequest{
IdpIntentId: intentID,
IdpIntentToken: "",
},
},
wantErr: true,
},
{
name: "wrong token",
args: args{
OrgCTX,
&user.RetrieveIdentityProviderIntentRequest{
IdpIntentId: successfulID,
IdpIntentToken: "wrong token",
},
},
wantErr: true,
},
{
name: "retrieve successful oauth intent",
args: args{
OrgCTX,
&user.RetrieveIdentityProviderIntentRequest{
IdpIntentId: successfulID,
IdpIntentToken: token,
},
},
want: &user.RetrieveIdentityProviderIntentResponse{
Details: &object.Details{
ChangeDate: timestamppb.New(changeDate),
ResourceOwner: Instance.ID(),
Sequence: sequence,
},
IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Oauth{
Oauth: &user.IDPOAuthAccessInformation{
AccessToken: "accessToken",
RefreshToken: gu.Ptr("refreshToken"),
IdToken: gu.Ptr("idToken"),
},
},
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
}(),
},
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{}},
},
},
UserAction: &user.RetrieveIdentityProviderIntentResponse_CreateUser{
CreateUser: &user.CreateUserRequest{
UserType: &user.CreateUserRequest_Human_{
Human: &user.CreateUserRequest_Human{
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,
},
{
name: "retrieve successful intent with linked user",
args: args{
OrgCTX,
&user.RetrieveIdentityProviderIntentRequest{
IdpIntentId: successfulWithUserID,
IdpIntentToken: withUsertoken,
},
},
want: &user.RetrieveIdentityProviderIntentResponse{
Details: &object.Details{
ChangeDate: timestamppb.New(withUserchangeDate),
ResourceOwner: Instance.ID(),
Sequence: withUsersequence,
},
UserId: "user",
IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Oauth{
Oauth: &user.IDPOAuthAccessInformation{
AccessToken: "accessToken",
RefreshToken: gu.Ptr("refreshToken"),
IdToken: gu.Ptr("idToken"),
},
},
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
}(),
},
UpdateHumanUser: &user.UpdateHumanUserRequest{
UserId: "user",
Profile: &user.SetHumanProfile{
PreferredLanguage: gu.Ptr("und"),
},
},
UserAction: &user.RetrieveIdentityProviderIntentResponse_UpdateUser{
UpdateUser: &user.UpdateUserRequest{
UserId: "user",
UserType: &user.UpdateUserRequest_Human_{
Human: &user.UpdateUserRequest_Human{
Profile: &user.UpdateUserRequest_Human_Profile{
PreferredLanguage: gu.Ptr("und"),
},
},
},
},
},
},
wantErr: false,
},
{
name: "retrieve successful expired intent",
args: args{
OrgCTX,
&user.RetrieveIdentityProviderIntentRequest{
IdpIntentId: successfulExpiredID,
IdpIntentToken: expiredToken,
},
},
wantErr: true,
},
{
name: "retrieve successful consumed intent",
args: args{
OrgCTX,
&user.RetrieveIdentityProviderIntentRequest{
IdpIntentId: successfulConsumedID,
IdpIntentToken: consumedToken,
},
},
wantErr: true,
},
{
name: "retrieve successful azure AD intent",
args: args{
OrgCTX,
&user.RetrieveIdentityProviderIntentRequest{
IdpIntentId: azureADSuccessful,
IdpIntentToken: azureADToken,
},
},
want: &user.RetrieveIdentityProviderIntentResponse{
Details: &object.Details{
ChangeDate: timestamppb.New(azureADChangeDate),
ResourceOwner: Instance.ID(),
Sequence: azureADSequence,
},
IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Oauth{
Oauth: &user.IDPOAuthAccessInformation{
AccessToken: "accessToken",
RefreshToken: gu.Ptr("refreshToken"),
IdToken: gu.Ptr("idToken"),
},
},
IdpId: azureIdpID,
UserId: "id",
UserName: "username",
RawInformation: func() *structpb.Struct {
s, err := structpb.NewStruct(map[string]interface{}{
"id": "id",
"userPrincipalName": "username",
"displayName": "displayname",
"givenName": "firstname",
"surname": "lastname",
"mail": "[email protected]",
})
require.NoError(t, err)
return s
}(),
},
AddHumanUser: &user.AddHumanUserRequest{
Username: gu.Ptr("username"),
Profile: &user.SetHumanProfile{
PreferredLanguage: gu.Ptr("und"),
GivenName: "firstname",
FamilyName: "lastname",
DisplayName: gu.Ptr("displayname"),
},
IdpLinks: []*user.IDPLink{
{IdpId: azureIdpID, UserId: "id", UserName: "username"},
},
Email: &user.SetHumanEmail{
Email: "[email protected]",
Verification: &user.SetHumanEmail_SendCode{SendCode: &user.SendEmailVerificationCode{}},
},
},
UserAction: &user.RetrieveIdentityProviderIntentResponse_CreateUser{
CreateUser: &user.CreateUserRequest{
Username: gu.Ptr("username"),
UserType: &user.CreateUserRequest_Human_{
Human: &user.CreateUserRequest_Human{
Profile: &user.SetHumanProfile{
PreferredLanguage: gu.Ptr("und"),
GivenName: "firstname",
FamilyName: "lastname",
DisplayName: gu.Ptr("displayname"),
},
IdpLinks: []*user.IDPLink{
{IdpId: azureIdpID, UserId: "id", UserName: "username"},
},
Email: &user.SetHumanEmail{
Email: "[email protected]",
Verification: &user.SetHumanEmail_SendCode{SendCode: &user.SendEmailVerificationCode{}},
},
},
},
},
},
},
wantErr: false,
},
{
name: "retrieve successful azure AD intent with user ID",
args: args{
OrgCTX,
&user.RetrieveIdentityProviderIntentRequest{
IdpIntentId: azureADSuccessfulWithUserID,
IdpIntentToken: azureADWithUserIDToken,
},
},
want: &user.RetrieveIdentityProviderIntentResponse{
Details: &object.Details{
ChangeDate: timestamppb.New(azureADWithUserIDChangeDate),
ResourceOwner: Instance.ID(),
Sequence: azureADWithUserIDSequence,
},
UserId: "user",
IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Oauth{
Oauth: &user.IDPOAuthAccessInformation{
AccessToken: "accessToken",
RefreshToken: gu.Ptr("refreshToken"),
IdToken: gu.Ptr("idToken"),
},
},
IdpId: azureIdpID,
UserId: "id",
UserName: "username",
RawInformation: func() *structpb.Struct {
s, err := structpb.NewStruct(map[string]interface{}{
"id": "id",
"userPrincipalName": "username",
"displayName": "displayname",
"givenName": "firstname",
"surname": "lastname",
"mail": "[email protected]",
})
require.NoError(t, err)
return s
}(),
},
UpdateHumanUser: &user.UpdateHumanUserRequest{
Username: gu.Ptr("username"),
UserId: "user",
Profile: &user.SetHumanProfile{
PreferredLanguage: gu.Ptr("und"),
GivenName: "firstname",
FamilyName: "lastname",
DisplayName: gu.Ptr("displayname"),
},
Email: &user.SetHumanEmail{
Email: "[email protected]",
Verification: &user.SetHumanEmail_SendCode{SendCode: &user.SendEmailVerificationCode{}},
},
},
UserAction: &user.RetrieveIdentityProviderIntentResponse_UpdateUser{
UpdateUser: &user.UpdateUserRequest{
Username: gu.Ptr("username"),
UserId: "user",
UserType: &user.UpdateUserRequest_Human_{
Human: &user.UpdateUserRequest_Human{
Profile: &user.UpdateUserRequest_Human_Profile{
PreferredLanguage: gu.Ptr("und"),
GivenName: gu.Ptr("firstname"),
FamilyName: gu.Ptr("lastname"),
DisplayName: gu.Ptr("displayname"),
},
Email: &user.SetHumanEmail{
Email: "[email protected]",
Verification: &user.SetHumanEmail_SendCode{SendCode: &user.SendEmailVerificationCode{}},
},
},
},
},
},
},
wantErr: false,
},
{
name: "retrieve successful oidc intent",
args: args{
OrgCTX,
&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",
RefreshToken: gu.Ptr("refreshToken"),
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{}},
},
},
UserAction: &user.RetrieveIdentityProviderIntentResponse_CreateUser{
CreateUser: &user.CreateUserRequest{
Username: gu.Ptr("username"),
UserType: &user.CreateUserRequest_Human_{
Human: &user.CreateUserRequest_Human{
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{
OrgCTX,
&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",
RefreshToken: gu.Ptr("refreshToken"),
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
}(),
},
UpdateHumanUser: &user.UpdateHumanUserRequest{
Username: gu.Ptr("username"),
UserId: "user",
Profile: &user.SetHumanProfile{
PreferredLanguage: gu.Ptr("und"),
},
},
UserAction: &user.RetrieveIdentityProviderIntentResponse_UpdateUser{
UpdateUser: &user.UpdateUserRequest{
Username: gu.Ptr("username"),
UserId: "user",
UserType: &user.UpdateUserRequest_Human_{
Human: &user.UpdateUserRequest_Human{
Profile: &user.UpdateUserRequest_Human_Profile{
PreferredLanguage: gu.Ptr("und"),
},
},
},
},
},
},
wantErr: false,
},
{
name: "retrieve successful ldap intent",
args: args{
OrgCTX,
&user.RetrieveIdentityProviderIntentRequest{
IdpIntentId: ldapSuccessfulID,
IdpIntentToken: ldapToken,
},
},
want: &user.RetrieveIdentityProviderIntentResponse{
Details: &object.Details{
ChangeDate: timestamppb.New(ldapChangeDate),
ResourceOwner: Instance.ID(),
Sequence: ldapSequence,
},
IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Ldap{
Ldap: &user.IDPLDAPAccessInformation{
Attributes: func() *structpb.Struct {
s, err := structpb.NewStruct(map[string]interface{}{
"id": []interface{}{"id"},
"username": []interface{}{"username"},
"language": []interface{}{"en"},
})
require.NoError(t, err)
return s
}(),
},
},
IdpId: ldapIdpID,
UserId: "id",
UserName: "username",
RawInformation: func() *structpb.Struct {
s, err := structpb.NewStruct(map[string]interface{}{
"id": "id",
"preferredUsername": "username",
"preferredLanguage": "en",
})
require.NoError(t, err)
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{}},
},
},
UserAction: &user.RetrieveIdentityProviderIntentResponse_CreateUser{
CreateUser: &user.CreateUserRequest{
Username: gu.Ptr("username"),
UserType: &user.CreateUserRequest_Human_{
Human: &user.CreateUserRequest_Human{
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,
},
{
name: "retrieve successful ldap intent with linked user",
args: args{
OrgCTX,
&user.RetrieveIdentityProviderIntentRequest{
IdpIntentId: ldapSuccessfulWithUserID,
IdpIntentToken: ldapWithUserToken,
},
},
want: &user.RetrieveIdentityProviderIntentResponse{
Details: &object.Details{
ChangeDate: timestamppb.New(ldapWithUserChangeDate),
ResourceOwner: Instance.ID(),
Sequence: ldapWithUserSequence,
},
UserId: "user",
IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Ldap{
Ldap: &user.IDPLDAPAccessInformation{
Attributes: func() *structpb.Struct {
s, err := structpb.NewStruct(map[string]interface{}{
"id": []interface{}{"id"},
"username": []interface{}{"username"},
"language": []interface{}{"en"},
})
require.NoError(t, err)
return s
}(),
},
},
IdpId: ldapIdpID,
UserId: "id",
UserName: "username",
RawInformation: func() *structpb.Struct {
s, err := structpb.NewStruct(map[string]interface{}{
"id": "id",
"preferredUsername": "username",
"preferredLanguage": "en",
})
require.NoError(t, err)
return s
}(),
},
UpdateHumanUser: &user.UpdateHumanUserRequest{
Username: gu.Ptr("username"),
UserId: "user",
Profile: &user.SetHumanProfile{
PreferredLanguage: gu.Ptr("en"),
},
},
UserAction: &user.RetrieveIdentityProviderIntentResponse_UpdateUser{
UpdateUser: &user.UpdateUserRequest{
Username: gu.Ptr("username"),
UserId: "user",
UserType: &user.UpdateUserRequest_Human_{
Human: &user.UpdateUserRequest_Human{
Profile: &user.UpdateUserRequest_Human_Profile{
PreferredLanguage: gu.Ptr("en"),
},
},
},
},
},
},
wantErr: false,
},
{
name: "retrieve successful saml intent",
args: args{
OrgCTX,
&user.RetrieveIdentityProviderIntentRequest{
IdpIntentId: samlSuccessfulID,
IdpIntentToken: samlToken,
},
},
want: &user.RetrieveIdentityProviderIntentResponse{
Details: &object.Details{
ChangeDate: timestamppb.New(samlChangeDate),
ResourceOwner: Instance.ID(),
Sequence: samlSequence,
},
IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Saml{
Saml: &user.IDPSAMLAccessInformation{
Assertion: []byte(fmt.Sprintf(`<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><Conditions NotBefore="0001-01-01T00:00:00Z" NotOnOrAfter="%s"></Conditions></Assertion>`, expiryFormatted)),
},
},
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
}(),
},
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{}},
},
},
UserAction: &user.RetrieveIdentityProviderIntentResponse_CreateUser{
CreateUser: &user.CreateUserRequest{
UserType: &user.CreateUserRequest_Human_{
Human: &user.CreateUserRequest_Human{
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{
OrgCTX,
&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(fmt.Sprintf(`<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><Conditions NotBefore="0001-01-01T00:00:00Z" NotOnOrAfter="%s"></Conditions></Assertion>`, expiryFormatted)),
},
},
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",
UpdateHumanUser: &user.UpdateHumanUserRequest{
UserId: "user",
Profile: &user.SetHumanProfile{
PreferredLanguage: gu.Ptr("und"),
},
},
UserAction: &user.RetrieveIdentityProviderIntentResponse_UpdateUser{
UpdateUser: &user.UpdateUserRequest{
UserId: "user",
UserType: &user.UpdateUserRequest_Human_{
Human: &user.UpdateUserRequest_Human{
Profile: &user.UpdateUserRequest_Human_Profile{
PreferredLanguage: gu.Ptr("und"),
},
},
},
},
},
},
wantErr: false,
},
{
name: "retrieve successful jwt intent",
args: args{
OrgCTX,
&user.RetrieveIdentityProviderIntentRequest{
IdpIntentId: jwtSuccessfulID,
IdpIntentToken: jwtToken,
},
},
want: &user.RetrieveIdentityProviderIntentResponse{
Details: &object.Details{
ChangeDate: timestamppb.New(jwtChangeDate),
ResourceOwner: Instance.ID(),
Sequence: jwtSequence,
},
IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Oauth{
Oauth: &user.IDPOAuthAccessInformation{
IdToken: gu.Ptr("idToken"),
},
},
IdpId: jwtIdPID,
UserId: "id",
UserName: "",
RawInformation: func() *structpb.Struct {
s, err := structpb.NewStruct(map[string]interface{}{
"sub": "id",
})
require.NoError(t, err)
return s
}(),
},
AddHumanUser: &user.AddHumanUserRequest{
Profile: &user.SetHumanProfile{
PreferredLanguage: gu.Ptr("und"),
},
IdpLinks: []*user.IDPLink{
{IdpId: jwtIdPID, UserId: "id"},
},
Email: &user.SetHumanEmail{
Verification: &user.SetHumanEmail_SendCode{SendCode: &user.SendEmailVerificationCode{}},
},
},
UserAction: &user.RetrieveIdentityProviderIntentResponse_CreateUser{
CreateUser: &user.CreateUserRequest{
UserType: &user.CreateUserRequest_Human_{
Human: &user.CreateUserRequest_Human{
Profile: &user.SetHumanProfile{
PreferredLanguage: gu.Ptr("und"),
},
IdpLinks: []*user.IDPLink{
{IdpId: jwtIdPID, UserId: "id"},
},
Email: &user.SetHumanEmail{
Verification: &user.SetHumanEmail_SendCode{SendCode: &user.SendEmailVerificationCode{}},
},
},
},
},
},
},
wantErr: false,
},
{
name: "retrieve successful jwt intent with linked user",
args: args{
OrgCTX,
&user.RetrieveIdentityProviderIntentRequest{
IdpIntentId: jwtSuccessfulWithUserID,
IdpIntentToken: jwtWithUserToken,
},
},
want: &user.RetrieveIdentityProviderIntentResponse{
Details: &object.Details{
ChangeDate: timestamppb.New(jwtWithUserChangeDate),
ResourceOwner: Instance.ID(),
Sequence: jwtWithUserSequence,
},
IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Oauth{
Oauth: &user.IDPOAuthAccessInformation{
IdToken: gu.Ptr("idToken"),
},
},
IdpId: jwtIdPID,
UserId: "id",
UserName: "",
RawInformation: func() *structpb.Struct {
s, err := structpb.NewStruct(map[string]interface{}{
"sub": "id",
})
require.NoError(t, err)
return s
}(),
},
UserId: "user",
UpdateHumanUser: &user.UpdateHumanUserRequest{
UserId: "user",
Profile: &user.SetHumanProfile{
PreferredLanguage: gu.Ptr("und"),
},
},
UserAction: &user.RetrieveIdentityProviderIntentResponse_UpdateUser{
UpdateUser: &user.UpdateUserRequest{
UserId: "user",
UserType: &user.UpdateUserRequest_Human_{
Human: &user.UpdateUserRequest_Human{
Profile: &user.UpdateUserRequest_Human_Profile{
PreferredLanguage: gu.Ptr("und"),
},
},
},
},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.RetrieveIdentityProviderIntent(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.EqualExportedValues(t, tt.want, got)
})
}
}
func TestServer_StartIdentityProviderIntent_WithLoginHint(t *testing.T) {
oauthIdpResp := Instance.AddGenericOAuthProvider(IamCTX, integration.IDPName())
type args struct {
ctx context.Context
req *user.StartIdentityProviderIntentRequest
}
type want struct {
url string
parametersExisting []string
parametersEqual map[string]string
}
tests := []struct {
name string
args args
want want
wantErr bool
}{
{
name: "OAuth with login_hint",
args: args{
OrgCTX,
&user.StartIdentityProviderIntentRequest{
IdpId: oauthIdpResp.Id,
Content: &user.StartIdentityProviderIntentRequest_Urls{
Urls: &user.RedirectURLs{
SuccessUrl: "https://example.com/success",
FailureUrl: "https://example.com/failure",
LoginHint: "[email protected]",
},
},
},
},
want: want{
url: "https://example.com/oauth/v2/authorize",
parametersEqual: map[string]string{
"client_id": "clientID",
"redirect_uri": "http://" + Instance.Domain + ":8082/idps/callback",
"response_type": "code",
"scope": "openid profile email",
"login_hint": "[email protected]",
},
parametersExisting: []string{"state"},
},
wantErr: false,
},
{
name: "Without login_hint",
args: args{
OrgCTX,
&user.StartIdentityProviderIntentRequest{
IdpId: oauthIdpResp.Id,
Content: &user.StartIdentityProviderIntentRequest_Urls{
Urls: &user.RedirectURLs{
SuccessUrl: "https://example.com/success",
FailureUrl: "https://example.com/failure",
// No LoginHint provided
},
},
},
},
want: want{
url: "https://example.com/oauth/v2/authorize",
parametersEqual: map[string]string{
"client_id": "clientID",
"prompt": "select_account",
"redirect_uri": "http://" + Instance.Domain + ":8082/idps/callback",
"response_type": "code",
"scope": "openid profile email",
},
parametersExisting: []string{"state"},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.StartIdentityProviderIntent(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
authUrl, err := url.Parse(got.GetAuthUrl())
require.NoError(t, err)
assert.Equal(t, tt.want.url, authUrl.Scheme+"://"+authUrl.Host+authUrl.Path)
for _, param := range tt.want.parametersExisting {
assert.True(t, authUrl.Query().Has(param), "Expected parameter %s to exist", param)
}
for key, expectedValue := range tt.want.parametersEqual {
actualValue := authUrl.Query().Get(key)
assert.Equal(t, expectedValue, actualValue, "Parameter %s should have value %s but got %s", key, expectedValue, actualValue)
}
if tt.args.req.GetContent().(*user.StartIdentityProviderIntentRequest_Urls).Urls.LoginHint == "" {
assert.False(t, authUrl.Query().Has("login_hint"), "login_hint should not be in URL when not provided or empty")
}
})
}
}