From 2162f866ff2459e3498de0c454f80144fa676dfb Mon Sep 17 00:00:00 2001 From: Gayathri Vijayan <66356931+grvijayan@users.noreply.github.com> Date: Mon, 10 Nov 2025 09:50:36 +0100 Subject: [PATCH] fix(user): Updating user info when authenticating with external IDP (#11046) # Which Problems Are Solved User profile updates were not propagated when using External OIDC IDP + Login V2 # How the Problems Are Solved * `UpdateHumanUserRequest` is added to `RetrieveIdentityProviderIntentResponse` * `UpdateHumanUserRequest` is returned in the `RetrieveIdentityProviderIntentResponse` when the user already exists during external IDP auth, which is then used in the frontend to update the user info # Additional Changes * Moved integration tests related to user intent to a separate test file * Fix redirection after external IDP user registration # Additional Context - Closes #10838 - Follow up: https://github.com/zitadel/zitadel/issues/11053 --------- Co-authored-by: Max Peintner (cherry picked from commit d7e9eddb7650282c4df53f5e196ce05ec897567c) --- .../register-form-idp-incomplete.tsx | 6 +- apps/login/src/lib/server/idp-intent.test.ts | 15 +- apps/login/src/lib/server/idp-intent.ts | 10 +- .../user/v2/integration_test/intent_test.go | 1040 +++++++++++++++++ .../user/v2/integration_test/user_test.go | 977 ---------------- internal/api/grpc/user/v2/intent.go | 107 +- proto/zitadel/user/v2/user_service.proto | 1 + 7 files changed, 1139 insertions(+), 1017 deletions(-) create mode 100644 internal/api/grpc/user/v2/integration_test/intent_test.go diff --git a/apps/login/src/components/register-form-idp-incomplete.tsx b/apps/login/src/components/register-form-idp-incomplete.tsx index d6bd128bb76..30b6fd0164d 100644 --- a/apps/login/src/components/register-form-idp-incomplete.tsx +++ b/apps/login/src/components/register-form-idp-incomplete.tsx @@ -4,6 +4,7 @@ import { registerUserAndLinkToIDP } from "@/lib/server/register"; import { useState } from "react"; import { useTranslations } from "next-intl"; import { FieldValues, useForm } from "react-hook-form"; +import { useRouter } from "next/navigation"; import { Alert } from "./alert"; import { BackButton } from "./back-button"; import { Button, ButtonVariants } from "./button"; @@ -55,6 +56,7 @@ export function RegisterFormIDPIncomplete({ }); const t = useTranslations("register"); + const router = useRouter(); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); @@ -85,7 +87,9 @@ export function RegisterFormIDPIncomplete({ return; } - // If no error, the function has already handled the redirect + if (response && "redirect" in response && response.redirect) { + return router.push(response.redirect); + } } const { errors } = formState; diff --git a/apps/login/src/lib/server/idp-intent.test.ts b/apps/login/src/lib/server/idp-intent.test.ts index c5d2aad531f..c6e7721bc93 100644 --- a/apps/login/src/lib/server/idp-intent.test.ts +++ b/apps/login/src/lib/server/idp-intent.test.ts @@ -76,6 +76,17 @@ describe("processIDPCallback", () => { email: "test@example.com", }, }, + updateHumanUser: { + username: "testuser", + profile: { + givenName: "Test", + familyName: "User 1", + displayName: "Test User 1", + }, + email: { + email: "test@example.com", + }, + }, }; const defaultIdp = { @@ -257,8 +268,8 @@ describe("processIDPCallback", () => { serviceUrl: "https://api.example.com", request: expect.objectContaining({ userId: "user123", - profile: defaultIntent.addHumanUser.profile, - email: defaultIntent.addHumanUser.email, + profile: defaultIntent.updateHumanUser.profile, + email: defaultIntent.updateHumanUser.email, }), }); }); diff --git a/apps/login/src/lib/server/idp-intent.ts b/apps/login/src/lib/server/idp-intent.ts index 5ad301b08dc..7de6dd22fc8 100644 --- a/apps/login/src/lib/server/idp-intent.ts +++ b/apps/login/src/lib/server/idp-intent.ts @@ -119,7 +119,7 @@ export async function processIDPCallback({ console.log("[IDP Process] Intent retrieved successfully, processing business logic"); - const { idpInformation, userId, addHumanUser } = intent; + const { idpInformation, userId, addHumanUser, updateHumanUser } = intent; if (!idpInformation) { console.error("[IDP Process] IDP information missing"); @@ -161,15 +161,15 @@ export async function processIDPCallback({ // ============================================ if (userId && !link) { // Auto-update user if enabled - if (options?.isAutoUpdate && addHumanUser) { + if (options?.isAutoUpdate && updateHumanUser) { try { await updateHuman({ serviceUrl, request: create(UpdateHumanUserRequestSchema, { userId: userId, - profile: addHumanUser.profile, - email: addHumanUser.email, - phone: addHumanUser.phone, + profile: updateHumanUser.profile, + email: updateHumanUser.email, + phone: updateHumanUser.phone, }), }); console.log("[IDP Process] User auto-updated successfully"); diff --git a/internal/api/grpc/user/v2/integration_test/intent_test.go b/internal/api/grpc/user/v2/integration_test/intent_test.go new file mode 100644 index 00000000000..0624486a4e6 --- /dev/null +++ b/internal/api/grpc/user/v2/integration_test/intent_test.go @@ -0,0 +1,1040 @@ +//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" + + "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" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +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", + 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{}}, + }, + }, + }, + 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", + 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"), + }, + }, + }, + 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", + 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@email.com", + }) + 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@email.com", + 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", + 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@email.com", + }) + 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@email.com", + 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", + 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{ + 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", + 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"), + }, + }, + }, + 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{}}, + }, + }, + }, + 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"), + }, + }, + }, + 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(``, 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{}}, + }, + }, + }, + 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(``, 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"), + }, + }, + }, + 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{}}, + }, + }, + }, + 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"), + }, + }, + }, + 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) + }) + } +} \ No newline at end of file diff --git a/internal/api/grpc/user/v2/integration_test/user_test.go b/internal/api/grpc/user/v2/integration_test/user_test.go index b976045f1d5..069b2e862a8 100644 --- a/internal/api/grpc/user/v2/integration_test/user_test.go +++ b/internal/api/grpc/user/v2/integration_test/user_test.go @@ -6,7 +6,6 @@ import ( "context" "encoding/base64" "fmt" - "net/url" "testing" "time" @@ -16,11 +15,9 @@ import ( "github.com/zitadel/logging" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "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/auth" "github.com/zitadel/zitadel/pkg/grpc/idp" mgmt "github.com/zitadel/zitadel/pkg/grpc/management" @@ -1897,306 +1894,6 @@ func TestServer_DeleteUser(t *testing.T) { }) } } - -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 createVerifiedWebAuthNSession(ctx context.Context, t *testing.T, userID string) string { // check if user is already processed retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) @@ -2209,680 +1906,6 @@ func createVerifiedWebAuthNSession(ctx context.Context, t *testing.T, userID str return token } -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", - 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{}}, - }, - }, - }, - 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", - 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 - }(), - }, - }, - 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", - 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@email.com", - }) - 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@email.com", - 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", - 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@email.com", - }) - require.NoError(t, err) - return s - }(), - }, - }, - 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", - 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{ - 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", - 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 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{}}, - }, - }, - }, - 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 - }(), - }, - }, - 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(``, 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{}}, - }, - }, - }, - 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(``, 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", - }, - 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{}}, - }, - }, - }, - 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", - }, - 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 ctxFromNewUserWithRegisteredPasswordlessLegacy(t *testing.T) (context.Context, string, *auth.AddMyPasswordlessResponse) { userID := Instance.CreateHumanUser(OrgCTX).GetUserId() Instance.RegisterUserPasskey(OrgCTX, userID) diff --git a/internal/api/grpc/user/v2/intent.go b/internal/api/grpc/user/v2/intent.go index 5bb2f50a95f..850776729de 100644 --- a/internal/api/grpc/user/v2/intent.go +++ b/internal/api/grpc/user/v2/intent.go @@ -169,40 +169,42 @@ func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *connec if err != nil { return nil, err } + 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.InitUser()) + case *oauth.Provider: + idpUser, err = unmarshalRawIdpUser(intent.IDPUser, p.User()) + case *oidc.Provider: + idpUser, err = unmarshalIdpUser(intent.IDPUser, oidc.InitUser()) + case *jwt.Provider: + idpUser, err = unmarshalIdpUser(intent.IDPUser, jwt.InitUser()) + case *azuread.Provider: + idpUser, err = unmarshalIdpUser(intent.IDPUser, p.User()) + case *github.Provider: + idpUser, err = unmarshalIdpUser(intent.IDPUser, &github.User{}) + case *gitlab.Provider: + idpUser, err = unmarshalIdpUser(intent.IDPUser, oidc.InitUser()) + case *google.Provider: + idpUser, err = unmarshalIdpUser(intent.IDPUser, google.InitUser()) + 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 + } 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.InitUser()) - case *oauth.Provider: - idpUser, err = unmarshalRawIdpUser(intent.IDPUser, p.User()) - case *oidc.Provider: - idpUser, err = unmarshalIdpUser(intent.IDPUser, oidc.InitUser()) - case *jwt.Provider: - idpUser, err = unmarshalIdpUser(intent.IDPUser, jwt.InitUser()) - case *azuread.Provider: - idpUser, err = unmarshalIdpUser(intent.IDPUser, p.User()) - case *github.Provider: - idpUser, err = unmarshalIdpUser(intent.IDPUser, &github.User{}) - case *gitlab.Provider: - idpUser, err = unmarshalIdpUser(intent.IDPUser, oidc.InitUser()) - case *google.Provider: - idpUser, err = unmarshalIdpUser(intent.IDPUser, google.InitUser()) - 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) + } else { + idpIntent.UpdateHumanUser = idpUserToUpdateHumanUser(intent.UserID, idpUser) } return connect.NewResponse(idpIntent), nil } @@ -377,3 +379,44 @@ func idpUserToAddHumanUser(idpUser idp.User, idpID string) *user.AddHumanUserReq } return addHumanUser } + +func idpUserToUpdateHumanUser(userID string, idpUser idp.User) *user.UpdateHumanUserRequest { + updateHumanUser := &user.UpdateHumanUserRequest{ + UserId: userID, + Profile: &user.SetHumanProfile{ + GivenName: idpUser.GetFirstName(), + FamilyName: idpUser.GetLastName(), + }, + } + if username := idpUser.GetPreferredUsername(); username != "" { + updateHumanUser.Username = &username + } + if nickName := idpUser.GetNickname(); nickName != "" { + updateHumanUser.Profile.NickName = &nickName + } + if displayName := idpUser.GetDisplayName(); displayName != "" { + updateHumanUser.Profile.DisplayName = &displayName + } + if lang := idpUser.GetPreferredLanguage().String(); lang != "" { + updateHumanUser.Profile.PreferredLanguage = &lang + } + if email := string(idpUser.GetEmail()); email != "" { + updateHumanUser.Email = &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_SendCode{}, + } + if isEmailVerified := idpUser.IsEmailVerified(); isEmailVerified { + updateHumanUser.Email.Verification = &user.SetHumanEmail_IsVerified{IsVerified: isEmailVerified} + } + } + if phone := string(idpUser.GetPhone()); phone != "" { + updateHumanUser.Phone = &user.SetHumanPhone{ + Phone: phone, + Verification: &user.SetHumanPhone_SendCode{}, + } + if isPhoneVerified := idpUser.IsPhoneVerified(); isPhoneVerified { + updateHumanUser.Phone.Verification = &user.SetHumanPhone_IsVerified{IsVerified: isPhoneVerified} + } + } + return updateHumanUser +} diff --git a/proto/zitadel/user/v2/user_service.proto b/proto/zitadel/user/v2/user_service.proto index 1c9bb5f8e33..24a29246529 100644 --- a/proto/zitadel/user/v2/user_service.proto +++ b/proto/zitadel/user/v2/user_service.proto @@ -3093,6 +3093,7 @@ message RetrieveIdentityProviderIntentResponse{ } ]; AddHumanUserRequest add_human_user = 4; + UpdateHumanUserRequest update_human_user = 5; } message AddIDPLinkRequest{