From 2a2d5392a3e466cc4cc6ae881a962f1cef0cd44e Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:06:56 +0100 Subject: [PATCH] fix: correctly send links to login v2 in email notifications (#10711) # Which Problems Are Solved There were still some emails (passkey registration and domain claimed) sent with links pointing to login v1 even when the login v2 was enabled for the instance. Also while looking into the issue, it was discovered that some links pointing to login V2 were not correctly generated. # How the Problems Are Solved - Added default paths for passkey registration and domain claimed notifications - Fixed the existing paths to properly handle concatenation (resp. use `url.ResolveReference`) - Change their go types (from string) to `*url.URL` - Added a mapstructure hook for string to url - Removed unnecessary `InstanceSetupFeatures` and corresponding conversions - Refactored the methods on the `login.DefaultPaths` struct and added an interface to the `Commands` to only need to pass a single config (and not every method) - Added an `OriginURL` method to the `DomainCtx` to prevent going from url to string and back - Added the use of the templates in case of enabled login v2 for passkey registration and domain claimed) # Additional Changes None # Additional Context closes #10643 --------- Co-authored-by: Max Peintner Co-authored-by: Livio Spring Co-authored-by: Livio Spring Co-authored-by: Max Peintner Co-authored-by: Gayathri Vijayan <66356931+grvijayan@users.noreply.github.com> --- .../self-hosting/deploy/docker-compose.yaml | 2 +- .../src/app/(login)/passkey/set/page.tsx | 1 + .../login/src/components/register-passkey.tsx | 20 +- apps/login/src/lib/server/passkeys.ts | 8 +- cmd/defaults.yaml | 15 +- cmd/mirror/config.go | 1 + cmd/mirror/projections.go | 5 +- cmd/setup/03.go | 5 +- cmd/setup/config.go | 2 + cmd/setup/config_change.go | 4 +- cmd/setup/config_test.go | 8 +- cmd/setup/setup.go | 7 +- cmd/start/config.go | 1 + cmd/start/config_test.go | 8 +- cmd/start/start.go | 5 +- internal/api/http/request_context.go | 13 +- internal/api/ui/login/login.go | 129 +++++++-- internal/api/ui/login/login_test.go | 259 ++++++++++++++++-- internal/command/command.go | 27 +- internal/command/command_test.go | 37 +++ internal/command/instance.go | 51 +--- internal/command/instance_features.go | 9 +- internal/command/instance_test.go | 119 -------- internal/command/mock_login_paths.go | 96 +++++++ internal/command/org_domain_test.go | 27 +- internal/command/user.go | 2 + internal/command/user_human.go | 6 +- internal/command/user_human_email.go | 4 +- internal/command/user_human_email_test.go | 241 ++++++++++++++++ internal/command/user_human_test.go | 48 +++- internal/command/user_v2_email.go | 6 +- internal/command/user_v2_email_test.go | 109 ++++---- internal/command/user_v2_human.go | 4 +- internal/command/user_v2_human_test.go | 121 +++++--- internal/command/user_v2_model_test.go | 8 +- internal/command/user_v2_passkey.go | 4 + internal/command/user_v2_passkey_test.go | 192 +++++++++++-- internal/command/user_v2_password.go | 2 +- internal/command/user_v2_password_test.go | 47 ++-- internal/config/hook/string_to_url.go | 27 ++ .../notification/handlers/user_notifier.go | 13 +- .../handlers/user_notifier_legacy.go | 27 +- .../handlers/user_notifier_legacy_test.go | 5 +- .../handlers/user_notifier_test.go | 5 +- internal/notification/projections.go | 4 +- internal/notification/types/domain_claimed.go | 26 +- internal/repository/user/human_email.go | 10 - internal/repository/user/user.go | 3 + 48 files changed, 1323 insertions(+), 450 deletions(-) create mode 100644 internal/command/mock_login_paths.go create mode 100644 internal/config/hook/string_to_url.go diff --git a/apps/docs/content/self-hosting/deploy/docker-compose.yaml b/apps/docs/content/self-hosting/deploy/docker-compose.yaml index bf233dcf5..3eb9df424 100644 --- a/apps/docs/content/self-hosting/deploy/docker-compose.yaml +++ b/apps/docs/content/self-hosting/deploy/docker-compose.yaml @@ -36,7 +36,7 @@ services: # Activate the login v2 on an installation from scratch. # To activate the login v2 on an existing installation, read the "What's next" section. ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED: true # To use the login v1, set this to false. - ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_BASEURI: http://localhost:3000/ui/v2/login + ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_BASEURI: http://localhost:3000/ui/v2/login/ # Configure the redirection paths to the login v2. ZITADEL_OIDC_DEFAULTLOGINURLV2: http://localhost:3000/ui/v2/login/login?authRequest= ZITADEL_OIDC_DEFAULTLOGOUTURLV2: http://localhost:3000/ui/v2/login/logout?post_logout_redirect= diff --git a/apps/login/src/app/(login)/passkey/set/page.tsx b/apps/login/src/app/(login)/passkey/set/page.tsx index 29ed8a720..31d707919 100644 --- a/apps/login/src/app/(login)/passkey/set/page.tsx +++ b/apps/login/src/app/(login)/passkey/set/page.tsx @@ -109,6 +109,7 @@ export default async function Page(props: { searchParams: Promise )} diff --git a/apps/login/src/components/register-passkey.tsx b/apps/login/src/components/register-passkey.tsx index 1a619c51f..d0ac0113c 100644 --- a/apps/login/src/components/register-passkey.tsx +++ b/apps/login/src/components/register-passkey.tsx @@ -21,9 +21,19 @@ type Props = { organization?: string; code?: string; codeId?: string; + loginName?: string; }; -export function RegisterPasskey({ sessionId, userId, isPrompt, organization, requestId, code, codeId }: Props) { +export function RegisterPasskey({ + sessionId, + userId, + isPrompt, + organization, + requestId, + code, + codeId, + loginName: initialLoginName, +}: Props) { const { handleSubmit, formState } = useForm({ mode: "onChange", }); @@ -155,7 +165,7 @@ export function RegisterPasskey({ sessionId, userId, isPrompt, organization, req return; } - continueAndLogin(); + continueAndLogin(verificationResponse.loginName); }, [sessionId, userId, code]); // Auto-submit when code is provided (similar to VerifyForm) @@ -165,7 +175,7 @@ export function RegisterPasskey({ sessionId, userId, isPrompt, organization, req } }, [code, submitRegisterAndContinue]); - function continueAndLogin() { + function continueAndLogin(loginName?: string) { const params = new URLSearchParams(); if (organization) { @@ -184,6 +194,10 @@ export function RegisterPasskey({ sessionId, userId, isPrompt, organization, req params.set("userId", userId); } + if (loginName ?? initialLoginName) { + params.set("loginName", loginName ?? initialLoginName ?? ""); + } + router.push("/passkey?" + params); } diff --git a/apps/login/src/lib/server/passkeys.ts b/apps/login/src/lib/server/passkeys.ts index c99730d11..c3349127c 100644 --- a/apps/login/src/lib/server/passkeys.ts +++ b/apps/login/src/lib/server/passkeys.ts @@ -196,6 +196,7 @@ export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) { }${os.name}${os.name ? ", " : ""}${browser.name}`; } + let loginName: string | undefined; let currentUserId: string; if (command.sessionId) { @@ -216,6 +217,7 @@ export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) { } currentUserId = userId; + loginName = session?.session?.factors?.user?.loginName; } else { // UserId-based flow currentUserId = command.userId!; @@ -226,9 +228,11 @@ export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) { if (!userResponse || !userResponse.user) { throw new Error("User not found"); } + + loginName = userResponse.user.preferredLoginName; } - return zitadelVerifyPasskeyRegistration({ + const response = await zitadelVerifyPasskeyRegistration({ serviceConfig, request: create(VerifyPasskeyRegistrationRequestSchema, { passkeyId: command.passkeyId, @@ -237,6 +241,8 @@ export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) { userId: currentUserId, }), }); + + return { ...response, loginName }; } type SendPasskeyCommand = { diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index ba89e4538..579e09e47 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -708,10 +708,17 @@ Login: # 168h is 7 days, one week SharedMaxAge: 168h # ZITADEL_LOGIN_CACHE_SHAREDMAXAGE DefaultPaths: - BasePath: "/ui/v2/login" - PasswordSetPath: "/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}" - EmailCodePath: "/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}" - OTPEmailPath: "/otp/verify?code={{.Code}}&userID={{.UserID}}&sessionId={{.SessionID}}" + BasePath: "/ui/v2/login/" + # PasswordSetPath is used to create the link for setting a password during user registration or password reset + PasswordSetPath: "password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}" # ZITADEL_LOGIN_DEFAULTPATHS_PASSWORDSETPATH + # EmailCodePath is used to create the link for verifying the user's email address + EmailCodePath: "verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}" + # OTPEmailPath is used to create the link for verifying the user's OTP during login + OTPEmailPath: "otp/email?code={{.Code}}&userId={{.UserID}}&sessionId={{.SessionID}}" + # PasskeySetPath is used to create the link for adding a passkey to the user's account though the login UI + PasskeySetPath: "passkey/set?codeId={{.CodeID}}&code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}" + # DomainClaimedPath is used to create the link to the login UI after an organization has claimed a domain and the user's loginname was updated + DomainClaimedPath: "loginname?loginName={{.TempUsername}}&organization={{.OrgID}}" Console: ShortCache: diff --git a/cmd/mirror/config.go b/cmd/mirror/config.go index 495ec91e9..78560201a 100644 --- a/cmd/mirror/config.go +++ b/cmd/mirror/config.go @@ -94,6 +94,7 @@ func newConfig(v *viper.Viper, config any) error { hooks.MapHTTPHeaderStringDecode, hook.Base64ToBytesHookFunc(), hook.TagToLanguageHookFunc(), + hook.StringToURLHookFunc(), mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToSliceHookFunc(","), diff --git a/cmd/mirror/projections.go b/cmd/mirror/projections.go index 9b9ae9a4a..ab4393262 100644 --- a/cmd/mirror/projections.go +++ b/cmd/mirror/projections.go @@ -214,8 +214,7 @@ func projections( config.OIDC.DefaultRefreshTokenExpiration, config.OIDC.DefaultRefreshTokenIdleExpiration, config.DefaultInstance.SecretGenerators, - nil, - nil, + config.Login.DefaultPaths, ) logging.OnError(ctx, err).Fatal("unable to start commands") @@ -239,7 +238,7 @@ func projections( commands, queries, es, - config.Login.DefaultPaths.OTPEmailPath, + config.Login.DefaultPaths.DefaultOTPEmailURLTemplate, config.SystemDefaults.Notifications.FileSystemPath, keys.User, keys.SMTP, diff --git a/cmd/setup/03.go b/cmd/setup/03.go index 29c7408ac..92e8e34c8 100644 --- a/cmd/setup/03.go +++ b/cmd/setup/03.go @@ -9,6 +9,7 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/ui/login" "github.com/zitadel/zitadel/internal/cache/connector" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/config/systemdefaults" @@ -43,6 +44,7 @@ type FirstInstance struct { externalSecure bool externalPort uint16 domain string + defaultPaths *login.DefaultPaths } func (mig *FirstInstance) Execute(ctx context.Context, _ eventstore.Event) error { @@ -92,8 +94,7 @@ func (mig *FirstInstance) Execute(ctx context.Context, _ eventstore.Event) error 0, 0, nil, - nil, - nil, + mig.defaultPaths, ) if err != nil { return err diff --git a/cmd/setup/config.go b/cmd/setup/config.go index b2d1ba71b..aa1a1554b 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -85,6 +85,7 @@ func NewConfig(cmd *cobra.Command, v *viper.Viper) (*Config, instrumentation.Shu hook.EnumHookFunc(authz.MemberTypeString), hook.Base64ToBytesHookFunc(), hook.TagToLanguageHookFunc(), + hook.StringToURLHookFunc(), mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToSliceHookFunc(","), @@ -205,6 +206,7 @@ func NewSteps(ctx context.Context, v *viper.Viper) (*Steps, error) { viper.DecodeHook(mapstructure.ComposeDecodeHookFunc( hook.Base64ToBytesHookFunc(), hook.TagToLanguageHookFunc(), + hook.StringToURLHookFunc(), mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToSliceHookFunc(","), diff --git a/cmd/setup/config_change.go b/cmd/setup/config_change.go index 8aeefa85b..8e0bebe48 100644 --- a/cmd/setup/config_change.go +++ b/cmd/setup/config_change.go @@ -3,6 +3,7 @@ package setup import ( "context" + "github.com/zitadel/zitadel/internal/api/ui/login" "github.com/zitadel/zitadel/internal/cache/connector" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/config/systemdefaults" @@ -58,8 +59,7 @@ func (mig *externalConfigChange) Execute(ctx context.Context, _ eventstore.Event 0, 0, nil, - nil, - nil, + &login.DefaultPaths{}, ) if err != nil { diff --git a/cmd/setup/config_test.go b/cmd/setup/config_test.go index faf5abafd..c14bd6f05 100644 --- a/cmd/setup/config_test.go +++ b/cmd/setup/config_test.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "fmt" "net/http" + "net/url" "strings" "testing" @@ -16,6 +17,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/feature" ) func TestMustNewConfig(t *testing.T) { @@ -48,12 +50,12 @@ Actions: DenyList: [] `}, want: func(t *testing.T, config *Config) { - assert.Equal(t, config.DefaultInstance.Features, &command.InstanceSetupFeatures{ + assert.Equal(t, config.DefaultInstance.Features, &command.InstanceFeatures{ LoginDefaultOrg: gu.Ptr(true), UserSchema: gu.Ptr(true), - LoginV2: &command.InstanceSetupFeatureLoginV2{ + LoginV2: &feature.LoginV2{ Required: true, - BaseURI: gu.Ptr("http://zitadel:8080"), + BaseURI: &url.URL{Scheme: "http", Host: "zitadel:8080"}, }, }) }, diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index c607923cf..e6b78189d 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -186,6 +186,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.FirstInstance.externalDomain = config.ExternalDomain steps.FirstInstance.externalSecure = config.ExternalSecure steps.FirstInstance.externalPort = config.ExternalPort + steps.FirstInstance.defaultPaths = config.Login.DefaultPaths steps.s5LastFailed = &LastFailed{dbClient: dbClient.DB} steps.s6OwnerRemoveColumns = &OwnerRemoveColumns{dbClient: dbClient.DB} @@ -573,9 +574,7 @@ func startCommandsQueries( config.OIDC.DefaultRefreshTokenExpiration, config.OIDC.DefaultRefreshTokenIdleExpiration, config.DefaultInstance.SecretGenerators, - - nil, - nil, + config.Login.DefaultPaths, ) logging.OnError(ctx, err).Fatal("unable to start commands") @@ -599,7 +598,7 @@ func startCommandsQueries( commands, queries, eventstoreClient, - config.Login.DefaultPaths.OTPEmailPath, + config.Login.DefaultPaths.DefaultOTPEmailURLTemplate, config.SystemDefaults.Notifications.FileSystemPath, keys.User, keys.SMTP, diff --git a/cmd/start/config.go b/cmd/start/config.go index 719961f11..054d28acc 100644 --- a/cmd/start/config.go +++ b/cmd/start/config.go @@ -151,6 +151,7 @@ func readConfig(v *viper.Viper) (*Config, error) { hooks.SliceTypeStringDecode[*command.SetQuota], hook.Base64ToBytesHookFunc(), hook.TagToLanguageHookFunc(), + hook.StringToURLHookFunc(), mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToSliceHookFunc(","), diff --git a/cmd/start/config_test.go b/cmd/start/config_test.go index 17dcd1ceb..91d8b7274 100644 --- a/cmd/start/config_test.go +++ b/cmd/start/config_test.go @@ -5,6 +5,7 @@ import ( "fmt" "net" "net/http" + "net/url" "strings" "testing" @@ -17,6 +18,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/feature" ) func Test_readConfig(t *testing.T) { @@ -84,12 +86,12 @@ Actions: DenyList: [] `}, want: func(t *testing.T, config *Config) { - assert.Equal(t, config.DefaultInstance.Features, &command.InstanceSetupFeatures{ + assert.Equal(t, config.DefaultInstance.Features, &command.InstanceFeatures{ LoginDefaultOrg: gu.Ptr(true), UserSchema: gu.Ptr(true), - LoginV2: &command.InstanceSetupFeatureLoginV2{ + LoginV2: &feature.LoginV2{ Required: true, - BaseURI: gu.Ptr("http://zitadel:8080"), + BaseURI: &url.URL{Scheme: "http", Host: "zitadel:8080"}, }, }) }, diff --git a/cmd/start/start.go b/cmd/start/start.go index 9f5aea9f3..74164cc5b 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -288,8 +288,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server config.OIDC.DefaultRefreshTokenExpiration, config.OIDC.DefaultRefreshTokenIdleExpiration, config.DefaultInstance.SecretGenerators, - config.Login.DefaultEmailCodeURLTemplate, - config.Login.DefaultPasswordSetURLTemplate, + config.Login.DefaultPaths, ) if err != nil { return fmt.Errorf("cannot start commands: %w", err) @@ -329,7 +328,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server commands, queries, eventstoreClient, - config.Login.DefaultPaths.OTPEmailPath, + config.Login.DefaultPaths.DefaultOTPEmailURLTemplate, config.SystemDefaults.Notifications.FileSystemPath, keys.User, keys.SMTP, diff --git a/internal/api/http/request_context.go b/internal/api/http/request_context.go index 1b93cce83..ad83b2829 100644 --- a/internal/api/http/request_context.go +++ b/internal/api/http/request_context.go @@ -61,11 +61,16 @@ func (r *DomainCtx) RequestedDomain() string { // Origin returns the origin (protocol://hostname[:port]) for which the request was handled. // The instance host is used if no public host was set. func (r *DomainCtx) Origin() string { - host := r.PublicHost - if host == "" { - host = r.InstanceHost + return fmt.Sprintf("%s://%s", r.Protocol, r.RequestedHost()) +} + +// OriginURL returns the origin (protocol://hostname[:port]) for which the request was handled as [*url.URL]. +// The instance host is used if no public host was set. +func (r *DomainCtx) OriginURL() *url.URL { + return &url.URL{ + Scheme: r.Protocol, + Host: r.RequestedHost(), } - return fmt.Sprintf("%s://%s", r.Protocol, host) } var _ slog.LogValuer = (*DomainCtx)(nil) diff --git a/internal/api/ui/login/login.go b/internal/api/ui/login/login.go index c18e6fb1d..a573e6fcb 100644 --- a/internal/api/ui/login/login.go +++ b/internal/api/ui/login/login.go @@ -4,6 +4,7 @@ import ( "context" "net/http" "net/url" + "slices" "strings" "time" @@ -56,39 +57,125 @@ type Config struct { } type DefaultPaths struct { - BasePath string - PasswordSetPath string - EmailCodePath string - OTPEmailPath string + BasePath *url.URL + PasswordSetPath *url.URL + EmailCodePath *url.URL + OTPEmailPath *url.URL + PasskeySetPath *url.URL + DomainClaimedPath *url.URL } -func (c *Config) defaultBaseURL(ctx context.Context) string { +func (c *DefaultPaths) defaultBaseURL(ctx context.Context) *url.URL { loginV2 := authz.GetInstance(ctx).Features().LoginV2 - if loginV2.Required { - // use the origin as default - baseURI := http_utils.DomainContext(ctx).Origin() - // use custom base URI if defined - if loginV2.BaseURI != nil && loginV2.BaseURI.String() != "" { - baseURI = loginV2.BaseURI.String() - } - return baseURI + c.DefaultPaths.BasePath + // In case login v1 is still active, we don't want to return a base URL, as the templates will not be used + if !loginV2.Required { + return nil } - return "" + origin := http_utils.DomainContext(ctx).OriginURL() + if loginV2.BaseURI == nil || loginV2.BaseURI.String() == "" { + // In case the login v2 is enabled without a custom BaseURI, + // we use the request origin plus the default base path as the base URL for the templates. + if c.BasePath == nil { + return origin + } + return origin.ResolveReference(c.BasePath) + } + // In case a custom BaseURI is set for login v2, we use it as the base URL for the templates. + if loginV2.BaseURI.IsAbs() { + return loginV2.BaseURI + } + // If the custom BaseURI is a relative URL, we join the request origin with the custom BaseURI to form the base URL for the templates. + return origin.ResolveReference(loginV2.BaseURI) } -func (c *Config) DefaultEmailCodeURLTemplate(ctx context.Context) string { - basePath := c.defaultBaseURL(ctx) - if basePath == "" { +// mergeURLs will merge two URLs, where the path is joined and query parameters are combined. +// It uses (*url.URL).JoinPath on the base URL and the second URL's Path field to build the resulting path, +// and then merges the query parameters from both URLs using [mergeQueries], preserving existing values and placeholders. +// Fragments are not modified or combined by this function. +// For example, merging "https://example.com/base" with a URL whose path is "/path/to/resource" will result in "https://example.com/base/path/to/resource". +func mergeURLs(base, path *url.URL) string { + if base == nil && path == nil { return "" } - return basePath + c.DefaultPaths.EmailCodePath + if base == nil { + return path.String() + } + if path == nil { + return base.String() + } + u := base.JoinPath(path.Path) + u.RawQuery = mergeQueries(u.Query(), path.Query()) + return u.String() } -func (c *Config) DefaultPasswordSetURLTemplate(ctx context.Context) string { + +// mergeQueries will merge two sets of query parameters, preserving existing values and placeholders. +// It takes two url.Values, where the first one is considered the base and the second one is merged into it. +// The resulting query string will contain all unique key-value pairs from both sets of query parameters. +// If a value contains placeholders in the format "{{placeholder}}", they are preserved in the final query string without being URL-encoded. +func mergeQueries(base, path url.Values) string { + placeholders := map[string]string{} + for _, value := range base { + for _, v := range value { + if strings.Contains(v, "{{") && strings.Contains(v, "}}") { + placeholders[url.QueryEscape(v)] = v + } + } + } + for key, value := range path { + baseValues, ok := base[key] + for _, v := range value { + if !ok || !slices.Contains(baseValues, v) { + base.Add(key, v) + } + if strings.Contains(v, "{{") && strings.Contains(v, "}}") { + placeholders[url.QueryEscape(v)] = v + } + } + } + raw := base.Encode() + for esc, orig := range placeholders { + raw = strings.ReplaceAll(raw, esc, orig) + } + return raw +} + +func (c *DefaultPaths) DefaultEmailCodeURLTemplate(ctx context.Context) string { basePath := c.defaultBaseURL(ctx) - if basePath == "" { + if basePath == nil { return "" } - return c.defaultBaseURL(ctx) + c.DefaultPaths.PasswordSetPath + return mergeURLs(basePath, c.EmailCodePath) +} + +func (c *DefaultPaths) DefaultPasswordSetURLTemplate(ctx context.Context) string { + basePath := c.defaultBaseURL(ctx) + if basePath == nil { + return "" + } + return mergeURLs(basePath, c.PasswordSetPath) +} + +func (c *DefaultPaths) DefaultPasskeySetURLTemplate(ctx context.Context) string { + basePath := c.defaultBaseURL(ctx) + if basePath == nil { + return "" + } + return mergeURLs(basePath, c.PasskeySetPath) +} + +func (c *DefaultPaths) DefaultDomainClaimedURLTemplate(ctx context.Context) string { + basePath := c.defaultBaseURL(ctx) + if basePath == nil { + return "" + } + return mergeURLs(basePath, c.DomainClaimedPath) +} + +func (c *DefaultPaths) DefaultOTPEmailURLTemplate(origin *url.URL) string { + if c.BasePath == nil { + return mergeURLs(origin, c.OTPEmailPath) + } + return mergeURLs(origin.ResolveReference(c.BasePath), c.OTPEmailPath) } const ( diff --git a/internal/api/ui/login/login_test.go b/internal/api/ui/login/login_test.go index 4cc4fa958..a545ad1f9 100644 --- a/internal/api/ui/login/login_test.go +++ b/internal/api/ui/login/login_test.go @@ -16,12 +16,10 @@ import ( func TestConfig_defaultBaseURL(t *testing.T) { t.Parallel() - config := &Config{ - DefaultPaths: &DefaultPaths{BasePath: "/basepath"}, - } + config := &DefaultPaths{BasePath: &url.URL{Path: "/basepath"}} baseCustomURI, err := url.Parse("https://custom") - require.Nil(t, err) + require.NoError(t, err) tt := []struct { name string @@ -45,15 +43,14 @@ func TestConfig_defaultBaseURL(t *testing.T) { expected: "https://origin/basepath", }, { - name: "LoginV2 required, custom BaseURI", + name: "LoginV2 required, absolute custom BaseURI", inputCtx: http.WithDomainContext( authz.NewMockContext("instance1", "org1", "user1", authz.WithMockFeatures(feature.Features{LoginV2: feature.LoginV2{Required: true, BaseURI: baseCustomURI}}), ), &http.DomainCtx{Protocol: "https", PublicHost: "origin"}, ), - - expected: "https://custom/basepath", + expected: "https://custom", }, { name: "LoginV2 required, custom BaseURI empty string", @@ -65,13 +62,27 @@ func TestConfig_defaultBaseURL(t *testing.T) { ), expected: "https://origin/basepath", }, + { + name: "LoginV2 required, relative custom BaseURI", + inputCtx: http.WithDomainContext( + authz.NewMockContext("instance1", "org1", "user1", + authz.WithMockFeatures(feature.Features{LoginV2: feature.LoginV2{Required: true, BaseURI: &url.URL{Path: "/custom"}}}), + ), + &http.DomainCtx{Protocol: "https", PublicHost: "origin"}, + ), + expected: "https://origin/custom", + }, } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { t.Parallel() result := config.defaultBaseURL(tc.inputCtx) - assert.Equal(t, tc.expected, result) + if tc.expected == "" { + assert.Nil(t, result) + return + } + assert.Equal(t, tc.expected, result.String()) }) } } @@ -111,10 +122,9 @@ func TestConfig_DefaultEmailCodeURLTemplate(t *testing.T) { t.Parallel() // Given - c := &Config{ - DefaultPaths: &DefaultPaths{ - BasePath: "/basepath", - EmailCodePath: "/email-code-path"}, + c := &DefaultPaths{ + BasePath: &url.URL{Path: "/basepath"}, + EmailCodePath: &url.URL{Path: "/email-code-path"}, } // Test @@ -161,11 +171,9 @@ func TestConfig_DefaultPasswordSetURLTemplate(t *testing.T) { t.Parallel() // Given - c := &Config{ - DefaultPaths: &DefaultPaths{ - BasePath: "/basepath", - PasswordSetPath: "/password-set-path", - }, + c := &DefaultPaths{ + BasePath: &url.URL{Path: "/basepath"}, + PasswordSetPath: &url.URL{Path: "/password-set-path"}, } // Test @@ -176,3 +184,220 @@ func TestConfig_DefaultPasswordSetURLTemplate(t *testing.T) { }) } } + +func TestConfig_DefaultPasskeySetURLTemplate(t *testing.T) { + t.Parallel() + + tt := []struct { + testName string + inputCtx context.Context + expectedEmailURLTemplate string + }{ + { + testName: "when base path is empty should return empty email url template", + inputCtx: http.WithDomainContext( + authz.NewMockContext("instance1", "org1", "user1", + authz.WithMockFeatures(feature.Features{LoginV2: feature.LoginV2{Required: false, BaseURI: &url.URL{}}}), + ), + &http.DomainCtx{Protocol: "https", PublicHost: "origin"}, + ), + expectedEmailURLTemplate: "", + }, + { + testName: "when base path is not empty should return expected url template", + inputCtx: http.WithDomainContext( + authz.NewMockContext("instance1", "org1", "user1", + authz.WithMockFeatures(feature.Features{LoginV2: feature.LoginV2{Required: true, BaseURI: &url.URL{}}}), + ), + &http.DomainCtx{Protocol: "https", PublicHost: "origin"}, + ), + expectedEmailURLTemplate: "https://origin/basepath/passkey-set-path", + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // Given + c := &DefaultPaths{ + BasePath: &url.URL{Path: "/basepath"}, + PasskeySetPath: &url.URL{Path: "/passkey-set-path"}, + } + + // Test + res := c.DefaultPasskeySetURLTemplate(tc.inputCtx) + + // Verify + assert.Equal(t, tc.expectedEmailURLTemplate, res) + }) + } +} + +func TestConfig_DefaultDomainClaimedURLTemplate(t *testing.T) { + t.Parallel() + + tt := []struct { + testName string + inputCtx context.Context + expectedEmailURLTemplate string + }{ + { + testName: "when base path is empty should return empty email url template", + inputCtx: http.WithDomainContext( + authz.NewMockContext("instance1", "org1", "user1", + authz.WithMockFeatures(feature.Features{LoginV2: feature.LoginV2{Required: false, BaseURI: &url.URL{}}}), + ), + &http.DomainCtx{Protocol: "https", PublicHost: "origin"}, + ), + expectedEmailURLTemplate: "", + }, + { + testName: "when base path is not empty should return expected url template", + inputCtx: http.WithDomainContext( + authz.NewMockContext("instance1", "org1", "user1", + authz.WithMockFeatures(feature.Features{LoginV2: feature.LoginV2{Required: true, BaseURI: &url.URL{}}}), + ), + &http.DomainCtx{Protocol: "https", PublicHost: "origin"}, + ), + expectedEmailURLTemplate: "https://origin/basepath/loginname", + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // Given + c := &DefaultPaths{ + BasePath: &url.URL{Path: "/basepath"}, + DomainClaimedPath: &url.URL{Path: "/loginname"}, + } + + // Test + res := c.DefaultDomainClaimedURLTemplate(tc.inputCtx) + + // Verify + assert.Equal(t, tc.expectedEmailURLTemplate, res) + }) + } +} + +func Test_mergeURLs(t *testing.T) { + type args struct { + base *url.URL + path *url.URL + } + tests := []struct { + name string + args args + want string + }{ + { + name: "both base and path are nil, return empty string", + args: args{ + base: nil, + path: nil, + }, + want: "", + }, + { + name: "base is nil, path is set, return path", + args: args{ + base: nil, + path: &url.URL{Path: "/path"}, + }, + want: "/path", + }, + { + name: "base is set, path is nil, return base", + args: args{ + base: &url.URL{Path: "/base"}, + path: nil, + }, + want: "/base", + }, + { + name: "both base and path are set, return merged path", + args: args{ + base: &url.URL{Path: "/base"}, + path: &url.URL{Path: "/path"}, + }, + want: "/base/path", + }, + { + name: "both base and path are set incl. placeholders, return merged path", + args: args{ + base: &url.URL{Path: "/base", RawQuery: "query={{.Placeholder}}"}, + path: &url.URL{Path: "/path", RawQuery: "query={{.Placeholder1}}&query2={{.Placeholder2}}"}, + }, + want: "/base/path?query={{.Placeholder}}&query={{.Placeholder1}}&query2={{.Placeholder2}}", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := mergeURLs(tt.args.base, tt.args.path) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_mergeQueries(t *testing.T) { + type args struct { + base url.Values + path url.Values + } + tests := []struct { + name string + args args + want string + }{ + { + name: "both base and path are empty, return empty string", + args: args{ + base: map[string][]string{}, + path: map[string][]string{}, + }, + want: "", + }, + { + name: "base is empty, path is set, return path", + args: args{ + base: map[string][]string{}, + path: map[string][]string{ + "query": {"{{.Placeholder1}}", "{{.Placeholder2}}"}, + }, + }, + want: "query={{.Placeholder1}}&query={{.Placeholder2}}", + }, + { + name: "base is set, path is empty, return base", + args: args{ + base: map[string][]string{ + "query": {"{{.Placeholder}}"}, + }, + path: map[string][]string{}, + }, + want: "query={{.Placeholder}}", + }, + { + name: "both base and path are set, return merged query", + args: args{ + base: map[string][]string{ + "query": {"{{.Placeholder}}"}, + "query2": {"{{.Placeholder}}"}, + }, + path: map[string][]string{ + "query": {"{{.Placeholder1}}"}, + "query2": {"{{.Placeholder}}", "{{.Placeholder2}}"}, + }, + }, + want: "query={{.Placeholder}}&query={{.Placeholder1}}&query2={{.Placeholder}}&query2={{.Placeholder2}}", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, mergeQueries(tt.args.base, tt.args.path), "mergeQueries(%v, %v)", tt.args.base, tt.args.path) + }) + } +} diff --git a/internal/command/command.go b/internal/command/command.go index 29ef5adc8..104589139 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -98,11 +98,18 @@ type Commands struct { caches *Caches // Store instance IDs where all milestones are reached (except InstanceDeleted). // These instance's milestones never need to be invalidated, - // so the query and cache overhead can completely eliminated. + // so the query and cache overhead can be completely eliminated. milestonesCompleted sync.Map - defaultEmailCodeURLTemplate func(ctx context.Context) string - defaultPasswordSetURLTemplate func(ctx context.Context) string + loginPaths LoginPaths +} + +//go:generate mockgen -package command -destination ./mock_login_paths.go . LoginPaths +type LoginPaths interface { + DefaultEmailCodeURLTemplate(ctx context.Context) string + DefaultPasswordSetURLTemplate(ctx context.Context) string + DefaultPasskeySetURLTemplate(ctx context.Context) string + DefaultDomainClaimedURLTemplate(ctx context.Context) string } func StartCommands( @@ -120,12 +127,9 @@ func StartCommands( httpClient *http.Client, permissionCheck domain.PermissionCheck, sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error), - defaultAccessTokenLifetime, - defaultRefreshTokenLifetime, - defaultRefreshTokenIdleLifetime time.Duration, + defaultAccessTokenLifetime, defaultRefreshTokenLifetime, defaultRefreshTokenIdleLifetime time.Duration, defaultSecretGenerators *SecretGenerators, - defaultEmailCodeURLTemplate func(ctx context.Context) string, - defaultPasswordSetURLTemplate func(ctx context.Context) string, + loginPaths LoginPaths, ) (repo *Commands, err error) { if externalDomain == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Df21s", "no external domain specified") @@ -211,10 +215,9 @@ func StartCommands( WithHyphen: defaults.Multifactors.RecoveryCodes.WithHyphen, }, }, - GenerateDomain: domain.NewGeneratedInstanceDomain, - caches: caches, - defaultEmailCodeURLTemplate: defaultEmailCodeURLTemplate, - defaultPasswordSetURLTemplate: defaultPasswordSetURLTemplate, + GenerateDomain: domain.NewGeneratedInstanceDomain, + caches: caches, + loginPaths: loginPaths, } if defaultSecretGenerators != nil && defaultSecretGenerators.ClientSecret != nil { diff --git a/internal/command/command_test.go b/internal/command/command_test.go index 306f2abcf..850c25d53 100644 --- a/internal/command/command_test.go +++ b/internal/command/command_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/eventstore" @@ -144,3 +145,39 @@ func TestCommands_asyncPush(t *testing.T) { }) } } + +func expectLoginPathsNoCall(t *testing.T) LoginPaths { + return NewMockLoginPaths(gomock.NewController(t)) +} + +func expectLoginPathsDefaultEmailCodeURLTemplate(tmpl string) func(t *testing.T) LoginPaths { + return func(t *testing.T) LoginPaths { + m := NewMockLoginPaths(gomock.NewController(t)) + m.EXPECT().DefaultEmailCodeURLTemplate(gomock.Any()).Return(tmpl) + return m + } +} + +func expectLoginPathsDefaultDomainClaimedURLTemplate(tmpl string) func(t *testing.T) LoginPaths { + return func(t *testing.T) LoginPaths { + m := NewMockLoginPaths(gomock.NewController(t)) + m.EXPECT().DefaultDomainClaimedURLTemplate(gomock.Any()).Return(tmpl) + return m + } +} + +func expectLoginPathsDefaultPasswordSetURLTemplate(tmpl string) func(t *testing.T) LoginPaths { + return func(t *testing.T) LoginPaths { + m := NewMockLoginPaths(gomock.NewController(t)) + m.EXPECT().DefaultPasswordSetURLTemplate(gomock.Any()).Return(tmpl) + return m + } +} + +func expectLoginPathsDefaultPasskeySetURLTemplate(tmpl string) func(t *testing.T) LoginPaths { + return func(t *testing.T) LoginPaths { + m := NewMockLoginPaths(gomock.NewController(t)) + m.EXPECT().DefaultPasskeySetURLTemplate(gomock.Any()).Return(tmpl) + return m + } +} diff --git a/internal/command/instance.go b/internal/command/instance.go index 350a9875c..c9bd43291 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -2,7 +2,6 @@ package command import ( "context" - "net/url" "strings" "time" @@ -15,7 +14,6 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/feature" "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/notification/channels/smtp" @@ -124,57 +122,12 @@ type InstanceSetup struct { SMTPConfiguration *SMTPConfiguration OIDCSettings *OIDCSettings Quotas *SetQuotas - Features *InstanceSetupFeatures + Features *InstanceFeatures Limits *SetLimits Restrictions *SetRestrictions RolePermissionMappings []authz.RoleMapping } -type InstanceSetupFeatures struct { - LoginDefaultOrg *bool - UserSchema *bool - ImprovedPerformance []feature.ImprovedPerformanceType - DebugOIDCParentError *bool - OIDCSingleV1SessionTermination *bool - LoginV2 *InstanceSetupFeatureLoginV2 - PermissionCheckV2 *bool - ManagementConsoleUseV2UserApi *bool - EnableRelationalTables *bool -} - -type InstanceSetupFeatureLoginV2 struct { - Required bool `json:"required,omitempty"` - BaseURI *string `json:"base_uri,omitempty"` -} - -func (f *InstanceSetupFeatures) ToInstanceFeatures() (_ *InstanceFeatures, err error) { - if f == nil { - return nil, nil - } - var loginV2 *feature.LoginV2 - if f.LoginV2 != nil { - loginV2 = &feature.LoginV2{Required: f.LoginV2.Required} - if f.LoginV2.BaseURI != nil { - loginV2.BaseURI, err = url.Parse(*f.LoginV2.BaseURI) - if err != nil { - return nil, err - } - } - } - - return &InstanceFeatures{ - LoginDefaultOrg: f.LoginDefaultOrg, - UserSchema: f.UserSchema, - ImprovedPerformance: f.ImprovedPerformance, - DebugOIDCParentError: f.DebugOIDCParentError, - OIDCSingleV1SessionTermination: f.OIDCSingleV1SessionTermination, - LoginV2: loginV2, - PermissionCheckV2: f.PermissionCheckV2, - ManagementConsoleUseV2UserApi: f.ManagementConsoleUseV2UserApi, - EnableRelationalTables: f.EnableRelationalTables, - }, nil -} - type SMTPConfiguration struct { Description string SMTP smtp.SMTP @@ -468,7 +421,7 @@ func setupQuotas(commands *Commands, validations *[]preparation.Validation, setQ return nil } -func setupFeatures(validations *[]preparation.Validation, features *InstanceSetupFeatures, instanceID string) { +func setupFeatures(validations *[]preparation.Validation, features *InstanceFeatures, instanceID string) { if features != nil { *validations = append(*validations, prepareSetFeatures(instanceID, features)) } diff --git a/internal/command/instance_features.go b/internal/command/instance_features.go index 3100c49b1..806832606 100644 --- a/internal/command/instance_features.go +++ b/internal/command/instance_features.go @@ -56,16 +56,11 @@ func (c *Commands) SetInstanceFeatures(ctx context.Context, f *InstanceFeatures) return pushedEventsToObjectDetails(events), nil } -func prepareSetFeatures(instanceID string, f *InstanceSetupFeatures) preparation.Validation { +func prepareSetFeatures(instanceID string, f *InstanceFeatures) preparation.Validation { return func() (preparation.CreateCommands, error) { - features, err := f.ToInstanceFeatures() - if err != nil { - return nil, err - } - return func(ctx context.Context, _ preparation.FilterToQueryReducer) ([]eventstore.Command, error) { wm := NewInstanceFeaturesWriteModel(instanceID) - return wm.setCommands(ctx, features), nil + return wm.setCommands(ctx, f), nil }, nil } } diff --git a/internal/command/instance_test.go b/internal/command/instance_test.go index c5796f948..ca355d6fd 100644 --- a/internal/command/instance_test.go +++ b/internal/command/instance_test.go @@ -3,15 +3,12 @@ package command import ( "context" "encoding/json" - "net/url" "slices" "strings" "testing" "time" - "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "golang.org/x/text/language" @@ -21,7 +18,6 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/feature" "github.com/zitadel/zitadel/internal/id" id_mock "github.com/zitadel/zitadel/internal/id/mock" "github.com/zitadel/zitadel/internal/repository/instance" @@ -1769,118 +1765,3 @@ func TestCommandSide_RemoveInstance(t *testing.T) { }) } } - -func TestInstanceSetupFeatures_ToInstanceFeatures(t *testing.T) { - t.Parallel() - - type fields struct { - LoginDefaultOrg *bool - UserSchema *bool - ImprovedPerformance []feature.ImprovedPerformanceType - DebugOIDCParentError *bool - OIDCSingleV1SessionTermination *bool - LoginV2 *InstanceSetupFeatureLoginV2 - PermissionCheckV2 *bool - ManagementConsoleUseV2UserApi *bool - EnableRelationalTables *bool - } - - correctlyParsedURI, err := url.Parse("https://example.com") - require.NoError(t, err) - - tt := []struct { - name string - fields fields - want *InstanceFeatures - wantErr bool - }{ - { - name: "nil features returns nil", - fields: fields{}, - want: &InstanceFeatures{}, - }, - { - name: "all fields no login v2", - fields: fields{ - LoginDefaultOrg: gu.Ptr(true), - UserSchema: gu.Ptr(false), - ImprovedPerformance: []feature.ImprovedPerformanceType{feature.ImprovedPerformanceTypeOrgDomainVerified}, - DebugOIDCParentError: gu.Ptr(true), - OIDCSingleV1SessionTermination: gu.Ptr(false), - PermissionCheckV2: gu.Ptr(true), - ManagementConsoleUseV2UserApi: gu.Ptr(false), - EnableRelationalTables: gu.Ptr(true), - }, - want: &InstanceFeatures{ - LoginDefaultOrg: gu.Ptr(true), - UserSchema: gu.Ptr(false), - ImprovedPerformance: []feature.ImprovedPerformanceType{feature.ImprovedPerformanceTypeOrgDomainVerified}, - DebugOIDCParentError: gu.Ptr(true), - OIDCSingleV1SessionTermination: gu.Ptr(false), - LoginV2: nil, - PermissionCheckV2: gu.Ptr(true), - ManagementConsoleUseV2UserApi: gu.Ptr(false), - EnableRelationalTables: gu.Ptr(true), - }, - }, - { - name: "with login v2 no base uri", - fields: fields{ - LoginV2: &InstanceSetupFeatureLoginV2{ - Required: true, - }, - }, - want: &InstanceFeatures{ - LoginV2: &feature.LoginV2{ - Required: true, - }, - }, - }, - { - name: "with login v2 valid base uri", - fields: fields{ - LoginV2: &InstanceSetupFeatureLoginV2{ - Required: true, - BaseURI: gu.Ptr("https://example.com"), - }, - }, - want: &InstanceFeatures{ - LoginV2: &feature.LoginV2{ - Required: true, - BaseURI: correctlyParsedURI, - }, - }, - }, - { - name: "with login v2 invalid base uri", - fields: fields{ - LoginV2: &InstanceSetupFeatureLoginV2{ - Required: true, - BaseURI: gu.Ptr("://invalid"), - }, - }, - wantErr: true, - }, - } - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - f := &InstanceSetupFeatures{ - LoginDefaultOrg: tc.fields.LoginDefaultOrg, - UserSchema: tc.fields.UserSchema, - ImprovedPerformance: tc.fields.ImprovedPerformance, - DebugOIDCParentError: tc.fields.DebugOIDCParentError, - OIDCSingleV1SessionTermination: tc.fields.OIDCSingleV1SessionTermination, - LoginV2: tc.fields.LoginV2, - PermissionCheckV2: tc.fields.PermissionCheckV2, - ManagementConsoleUseV2UserApi: tc.fields.ManagementConsoleUseV2UserApi, - EnableRelationalTables: tc.fields.EnableRelationalTables, - } - got, err := f.ToInstanceFeatures() - - require.Equal(t, tc.wantErr, err != nil) - assert.Equal(t, tc.want, got) - }) - } -} diff --git a/internal/command/mock_login_paths.go b/internal/command/mock_login_paths.go new file mode 100644 index 000000000..718eed270 --- /dev/null +++ b/internal/command/mock_login_paths.go @@ -0,0 +1,96 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/zitadel/zitadel/internal/command (interfaces: LoginPaths) +// +// Generated by this command: +// +// mockgen -package command -destination ./mock_login_paths.go . LoginPaths +// + +// Package command is a generated GoMock package. +package command + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockLoginPaths is a mock of LoginPaths interface. +type MockLoginPaths struct { + ctrl *gomock.Controller + recorder *MockLoginPathsMockRecorder +} + +// MockLoginPathsMockRecorder is the mock recorder for MockLoginPaths. +type MockLoginPathsMockRecorder struct { + mock *MockLoginPaths +} + +// NewMockLoginPaths creates a new mock instance. +func NewMockLoginPaths(ctrl *gomock.Controller) *MockLoginPaths { + mock := &MockLoginPaths{ctrl: ctrl} + mock.recorder = &MockLoginPathsMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLoginPaths) EXPECT() *MockLoginPathsMockRecorder { + return m.recorder +} + +// DefaultDomainClaimedURLTemplate mocks base method. +func (m *MockLoginPaths) DefaultDomainClaimedURLTemplate(arg0 context.Context) string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DefaultDomainClaimedURLTemplate", arg0) + ret0, _ := ret[0].(string) + return ret0 +} + +// DefaultDomainClaimedURLTemplate indicates an expected call of DefaultDomainClaimedURLTemplate. +func (mr *MockLoginPathsMockRecorder) DefaultDomainClaimedURLTemplate(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DefaultDomainClaimedURLTemplate", reflect.TypeOf((*MockLoginPaths)(nil).DefaultDomainClaimedURLTemplate), arg0) +} + +// DefaultEmailCodeURLTemplate mocks base method. +func (m *MockLoginPaths) DefaultEmailCodeURLTemplate(arg0 context.Context) string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DefaultEmailCodeURLTemplate", arg0) + ret0, _ := ret[0].(string) + return ret0 +} + +// DefaultEmailCodeURLTemplate indicates an expected call of DefaultEmailCodeURLTemplate. +func (mr *MockLoginPathsMockRecorder) DefaultEmailCodeURLTemplate(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DefaultEmailCodeURLTemplate", reflect.TypeOf((*MockLoginPaths)(nil).DefaultEmailCodeURLTemplate), arg0) +} + +// DefaultPasskeySetURLTemplate mocks base method. +func (m *MockLoginPaths) DefaultPasskeySetURLTemplate(arg0 context.Context) string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DefaultPasskeySetURLTemplate", arg0) + ret0, _ := ret[0].(string) + return ret0 +} + +// DefaultPasskeySetURLTemplate indicates an expected call of DefaultPasskeySetURLTemplate. +func (mr *MockLoginPathsMockRecorder) DefaultPasskeySetURLTemplate(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DefaultPasskeySetURLTemplate", reflect.TypeOf((*MockLoginPaths)(nil).DefaultPasskeySetURLTemplate), arg0) +} + +// DefaultPasswordSetURLTemplate mocks base method. +func (m *MockLoginPaths) DefaultPasswordSetURLTemplate(arg0 context.Context) string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DefaultPasswordSetURLTemplate", arg0) + ret0, _ := ret[0].(string) + return ret0 +} + +// DefaultPasswordSetURLTemplate indicates an expected call of DefaultPasswordSetURLTemplate. +func (mr *MockLoginPathsMockRecorder) DefaultPasswordSetURLTemplate(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DefaultPasswordSetURLTemplate", reflect.TypeOf((*MockLoginPaths)(nil).DefaultPasswordSetURLTemplate), arg0) +} diff --git a/internal/command/org_domain_test.go b/internal/command/org_domain_test.go index 1c5ab30b1..a2cd5dc72 100644 --- a/internal/command/org_domain_test.go +++ b/internal/command/org_domain_test.go @@ -29,6 +29,7 @@ func TestAddDomain(t *testing.T) { idGenerator id.Generator filter preparation.FilterToQueryReducer eventstore func(*testing.T) *eventstore.Eventstore + loginPaths func(*testing.T) LoginPaths permissionCheck OrganizationPermissionCheck } @@ -45,6 +46,7 @@ func TestAddDomain(t *testing.T) { a: agg, domain: "", eventstore: expectEventstore(), + loginPaths: expectLoginPathsNoCall, }, want: Want{ ValidationErr: zerrors.ThrowInvalidArgument(nil, "ORG-r3h4J", "Errors.Invalid.Argument"), @@ -69,6 +71,7 @@ func TestAddDomain(t *testing.T) { ), ), ), + loginPaths: expectLoginPathsNoCall, }, want: Want{ Commands: []eventstore.Command{ @@ -118,12 +121,13 @@ func TestAddDomain(t *testing.T) { ), ), ), + loginPaths: expectLoginPathsDefaultDomainClaimedURLTemplate(""), }, want: Want{ Commands: []eventstore.Command{ org.NewDomainAddedEvent(context.Background(), &agg.Aggregate, "domain"), org.NewDomainVerifiedEvent(context.Background(), &agg.Aggregate, "domain"), - user.NewDomainClaimedEvent(http.WithRequestedHost(context.Background(), "domain"), &user.NewAggregate("userID1", "org2").Aggregate, "newID@temporary.domain", "username", false), + user.NewDomainClaimedEvent(http.WithRequestedHost(context.Background(), "domain"), &user.NewAggregate("userID1", "org2").Aggregate, "newID@temporary.domain", "username", false, ""), }, }, }, @@ -148,6 +152,7 @@ func TestAddDomain(t *testing.T) { ), ), ), + loginPaths: expectLoginPathsNoCall, }, want: Want{ CreateErr: zerrors.ThrowAlreadyExists(nil, "", ""), @@ -161,6 +166,7 @@ func TestAddDomain(t *testing.T) { claimedUserIDs: []string{"userID1"}, filter: nil, eventstore: expectEventstore(), + loginPaths: expectLoginPathsNoCall, permissionCheck: newMockOrganizationPermissionCheckNotAllowed(), }, want: Want{ @@ -186,6 +192,7 @@ func TestAddDomain(t *testing.T) { ), ), ), + loginPaths: expectLoginPathsNoCall, permissionCheck: newMockOrganizationPermissionCheckAllowed(), }, want: Want{ @@ -200,7 +207,7 @@ func TestAddDomain(t *testing.T) { AssertValidation( t, http.WithRequestedHost(context.Background(), "domain"), - (&Commands{idGenerator: tt.args.idGenerator, eventstore: tt.args.eventstore(t)}).prepareAddOrgDomain(tt.args.a, tt.args.domain, tt.args.claimedUserIDs, tt.args.permissionCheck), + (&Commands{idGenerator: tt.args.idGenerator, eventstore: tt.args.eventstore(t), loginPaths: tt.args.loginPaths(t)}).prepareAddOrgDomain(tt.args.a, tt.args.domain, tt.args.claimedUserIDs, tt.args.permissionCheck), tt.args.filter, tt.want, ) @@ -879,6 +886,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { secretGenerator crypto.Generator alg crypto.EncryptionAlgorithm domainValidationFunc func(domain, token, verifier string, checkType http.CheckType) error + loginPaths func(*testing.T) LoginPaths } type args struct { ctx context.Context @@ -900,6 +908,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { name: "invalid domain, error", fields: fields{ eventstore: expectEventstore(), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -917,6 +926,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { name: "missing aggregateid, error", fields: fields{ eventstore: expectEventstore(), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -941,6 +951,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { ), ), ), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -981,6 +992,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { ), ), ), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -1015,6 +1027,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { ), ), ), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -1070,6 +1083,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { ), alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), domainValidationFunc: invalidDomainVerification, + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -1125,6 +1139,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { ), alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), domainValidationFunc: validDomainVerification, + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -1183,6 +1198,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { ), alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), domainValidationFunc: validDomainVerification, + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -1264,12 +1280,14 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { "tempid@temporary.zitadel.ch", "username@domain.ch", false, + "", ), ), ), alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), domainValidationFunc: validDomainVerification, idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "tempid"), + loginPaths: expectLoginPathsDefaultDomainClaimedURLTemplate(""), }, args: args{ ctx: context.Background(), @@ -1351,12 +1369,14 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { "tempid@temporary.zitadel.ch", "username@domain.ch", true, + "", ), ), ), alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), domainValidationFunc: validDomainVerification, idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "tempid"), + loginPaths: expectLoginPathsDefaultDomainClaimedURLTemplate(""), }, args: args{ ctx: context.Background(), @@ -1415,6 +1435,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { ), alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), domainValidationFunc: validDomainVerification, + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -1439,6 +1460,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { eventstore: expectEventstore(), alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), domainValidationFunc: validDomainVerification, + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -1464,6 +1486,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { domainVerificationAlg: tt.fields.alg, domainVerificationValidator: tt.fields.domainValidationFunc, idGenerator: tt.fields.idGenerator, + loginPaths: tt.fields.loginPaths(t), } got, err := r.ValidateOrgDomain(http.WithRequestedHost(tt.args.ctx, "zitadel.ch"), tt.args.domain, tt.args.claimedUserIDs, tt.args.permissionCheck) if tt.res.err == nil { diff --git a/internal/command/user.go b/internal/command/user.go index ada16c8ff..93ea98414 100644 --- a/internal/command/user.go +++ b/internal/command/user.go @@ -303,6 +303,7 @@ func (c *Commands) userDomainClaimed(ctx context.Context, userID string) (events fmt.Sprintf("%s@temporary.%s", id, http_util.DomainContext(ctx).RequestedDomain()), existingUser.UserName, domainPolicy.UserLoginMustBeDomain || organizationScopedUsername, + c.loginPaths.DefaultDomainClaimedURLTemplate(ctx), ), }, changedUserGrant, nil } @@ -338,6 +339,7 @@ func (c *Commands) prepareUserDomainClaimed(ctx context.Context, filter preparat fmt.Sprintf("%s@temporary.%s", id, http_util.DomainContext(ctx).RequestedDomain()), userWriteModel.UserName, domainPolicy.UserLoginMustBeDomain || organizationScopedUsername, + c.loginPaths.DefaultDomainClaimedURLTemplate(ctx), ), nil } diff --git a/internal/command/user_human.go b/internal/command/user_human.go index eb693ea57..061cff7e9 100644 --- a/internal/command/user_human.go +++ b/internal/command/user_human.go @@ -306,10 +306,10 @@ func (c *Commands) addHumanCommandEmail(ctx context.Context, filter preparation. } if human.Email.URLTemplate == "" { - human.Email.URLTemplate = c.defaultEmailCodeURLTemplate(ctx) + human.Email.URLTemplate = c.loginPaths.DefaultEmailCodeURLTemplate(ctx) } - return append(cmds, user.NewHumanEmailCodeAddedEventV2(ctx, &a.Aggregate, emailCode.Crypted, emailCode.Expiry, human.Email.URLTemplate, human.Email.ReturnCode, human.AuthRequestID)), nil + return append(cmds, user.NewHumanEmailCodeAddedEvent(ctx, &a.Aggregate, emailCode.Crypted, emailCode.Expiry, human.Email.URLTemplate, human.Email.ReturnCode, human.AuthRequestID)), nil } return cmds, nil } @@ -599,7 +599,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. if err != nil { return nil, nil, nil, err } - events = append(events, user.NewHumanEmailCodeAddedEvent(ctx, userAgg, emailCode.Code, emailCode.Expiry, "")) + events = append(events, user.NewHumanEmailCodeAddedEvent(ctx, userAgg, emailCode.Code, emailCode.Expiry, c.loginPaths.DefaultEmailCodeURLTemplate(ctx), false, "")) } } diff --git a/internal/command/user_human_email.go b/internal/command/user_human_email.go index c954b3246..92fe8b95c 100644 --- a/internal/command/user_human_email.go +++ b/internal/command/user_human_email.go @@ -50,7 +50,7 @@ func (c *Commands) ChangeHumanEmail(ctx context.Context, email *domain.Email, em if err != nil { return nil, err } - events = append(events, user.NewHumanEmailCodeAddedEvent(ctx, userAgg, emailCode.Code, emailCode.Expiry, "")) + events = append(events, user.NewHumanEmailCodeAddedEvent(ctx, userAgg, emailCode.Code, emailCode.Expiry, c.loginPaths.DefaultEmailCodeURLTemplate(ctx), false, "")) } pushedEvents, err := c.eventstore.Push(ctx, events...) @@ -134,7 +134,7 @@ func (c *Commands) CreateHumanEmailVerificationCode(ctx context.Context, userID, if authRequestID == "" { authRequestID = existingEmail.AuthRequestID } - pushedEvents, err := c.eventstore.Push(ctx, user.NewHumanEmailCodeAddedEvent(ctx, userAgg, emailCode.Code, emailCode.Expiry, authRequestID)) + pushedEvents, err := c.eventstore.Push(ctx, user.NewHumanEmailCodeAddedEvent(ctx, userAgg, emailCode.Code, emailCode.Expiry, c.loginPaths.DefaultEmailCodeURLTemplate(ctx), false, authRequestID)) if err != nil { return nil, err } diff --git a/internal/command/user_human_email_test.go b/internal/command/user_human_email_test.go index e18413594..545ce3e0a 100644 --- a/internal/command/user_human_email_test.go +++ b/internal/command/user_human_email_test.go @@ -20,6 +20,7 @@ import ( func TestCommandSide_ChangeHumanEmail(t *testing.T) { type fields struct { eventstore func(*testing.T) *eventstore.Eventstore + loginPaths func(*testing.T) LoginPaths } type args struct { ctx context.Context @@ -41,6 +42,7 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) { name: "invalid email, invalid argument error", fields: fields{ eventstore: expectEventstore(), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -61,6 +63,7 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) { eventstore: expectEventstore( expectFilter(), ), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -104,6 +107,7 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) { ), ), ), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -140,6 +144,7 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) { ), ), ), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -185,6 +190,7 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) { ), ), ), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -234,6 +240,7 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) { ), ), ), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -283,6 +290,7 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) { ), ), ), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -341,9 +349,75 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) { }, time.Hour*1, "", + false, + "", ), ), ), + loginPaths: expectLoginPathsDefaultEmailCodeURLTemplate(""), + }, + args: args{ + ctx: context.Background(), + email: &domain.Email{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + EmailAddress: "email-changed@test.ch", + }, + resourceOwner: "org1", + secretGenerator: GetMockSecretGenerator(t), + }, + res: res{ + want: &domain.Email{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + EmailAddress: "email-changed@test.ch", + }, + }, + }, + { + name: "email changed with code, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectPush( + user.NewHumanEmailChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "email-changed@test.ch", + ), + user.NewHumanEmailCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + "http://example.com/{{.user}}/email/{{.code}}", + false, + "", + ), + ), + ), + loginPaths: expectLoginPathsDefaultEmailCodeURLTemplate("http://example.com/{{.user}}/email/{{.code}}"), }, args: args{ ctx: context.Background(), @@ -371,6 +445,7 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore(t), + loginPaths: tt.fields.loginPaths(t), } got, err := r.ChangeHumanEmail(tt.args.ctx, tt.args.email, tt.args.secretGenerator) if tt.res.err == nil { @@ -389,6 +464,7 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) { func TestCommandSide_VerifyHumanEmail(t *testing.T) { type fields struct { eventstore func(*testing.T) *eventstore.Eventstore + loginPaths func(*testing.T) LoginPaths userPasswordHasher *crypto.Hasher } type args struct { @@ -414,6 +490,7 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) { name: "userid missing, invalid argument error", fields: fields{ eventstore: expectEventstore(), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -428,6 +505,7 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) { name: "code missing, invalid argument error", fields: fields{ eventstore: expectEventstore(), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -444,6 +522,7 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) { eventstore: expectEventstore( expectFilter(), ), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -476,6 +555,7 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) { ), ), ), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -517,6 +597,8 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) { }, time.Hour*1, "", + false, + "", ), ), ), @@ -526,6 +608,7 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) { ), ), ), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -568,6 +651,8 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) { }, time.Hour*1, "", + false, + "", ), ), ), @@ -577,6 +662,7 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) { ), ), ), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -621,6 +707,8 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) { }, time.Hour*1, "", + false, + "", ), ), ), @@ -649,6 +737,84 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) { ), ), userPasswordHasher: mockPasswordHasher("x"), + loginPaths: expectLoginPathsNoCall, + }, + args: args{ + ctx: context.Background(), + userID: "user1", + code: "a", + resourceOwner: "org1", + optionalPassword: "password", + optionalUserAgentID: "userAgentID", + secretGenerator: GetMockSecretGenerator(t), + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "valid code (with password and user agent), url template, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusherWithCreationDateNow( + user.NewHumanEmailCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + "http://example.com/{{.user}}/email/{{.code}}", + false, + "", + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + user.NewHumanPasswordChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "$plain$x$password", + false, + "userAgentID", + ), + ), + ), + userPasswordHasher: mockPasswordHasher("x"), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -671,6 +837,7 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore(t), userPasswordHasher: tt.fields.userPasswordHasher, + loginPaths: tt.fields.loginPaths(t), } got, err := r.VerifyHumanEmail( tt.args.ctx, @@ -697,6 +864,7 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) { func TestCommandSide_CreateVerificationCodeHumanEmail(t *testing.T) { type fields struct { eventstore func(*testing.T) *eventstore.Eventstore + loginPaths func(*testing.T) LoginPaths } type args struct { ctx context.Context @@ -719,6 +887,7 @@ func TestCommandSide_CreateVerificationCodeHumanEmail(t *testing.T) { name: "userid missing, invalid argument error", fields: fields{ eventstore: expectEventstore(), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -734,6 +903,7 @@ func TestCommandSide_CreateVerificationCodeHumanEmail(t *testing.T) { eventstore: expectEventstore( expectFilter(), ), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -772,6 +942,7 @@ func TestCommandSide_CreateVerificationCodeHumanEmail(t *testing.T) { ), ), ), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -808,6 +979,7 @@ func TestCommandSide_CreateVerificationCodeHumanEmail(t *testing.T) { ), ), ), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -860,9 +1032,12 @@ func TestCommandSide_CreateVerificationCodeHumanEmail(t *testing.T) { }, time.Hour*1, "", + false, + "", ), ), ), + loginPaths: expectLoginPathsDefaultEmailCodeURLTemplate(""), }, args: args{ ctx: context.Background(), @@ -917,10 +1092,75 @@ func TestCommandSide_CreateVerificationCodeHumanEmail(t *testing.T) { Crypted: []byte("a"), }, time.Hour*1, + "", + false, "authRequestID", ), ), ), + loginPaths: expectLoginPathsDefaultEmailCodeURLTemplate(""), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + secretGenerator: GetMockSecretGenerator(t), + authRequestID: "authRequestID", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "new code with authRequestID, url template, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + eventFromEventPusher( + user.NewHumanEmailChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "email2@test.ch", + ), + ), + ), + expectPush( + user.NewHumanEmailCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + "http://example.com/{{.user}}/email/{{.code}}", + false, + "authRequestID", + ), + ), + ), + loginPaths: expectLoginPathsDefaultEmailCodeURLTemplate("http://example.com/{{.user}}/email/{{.code}}"), }, args: args{ ctx: context.Background(), @@ -940,6 +1180,7 @@ func TestCommandSide_CreateVerificationCodeHumanEmail(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore(t), + loginPaths: tt.fields.loginPaths(t), } got, err := r.CreateHumanEmailVerificationCode(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.secretGenerator, tt.args.authRequestID) if tt.res.err == nil { diff --git a/internal/command/user_human_test.go b/internal/command/user_human_test.go index 7b52ebc05..75b91225b 100644 --- a/internal/command/user_human_test.go +++ b/internal/command/user_human_test.go @@ -45,7 +45,7 @@ func TestCommandSide_AddHuman(t *testing.T) { newCode encrypedCodeFunc newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc defaultSecretGenerators *SecretGenerators - defaultEmailCodeURLTemplate func(ctx context.Context) string + loginPaths func(*testing.T) LoginPaths } type args struct { ctx context.Context @@ -73,6 +73,7 @@ func TestCommandSide_AddHuman(t *testing.T) { name: "orgid missing, invalid argument error", fields: fields{ eventstore: expectEventstore(), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -97,6 +98,7 @@ func TestCommandSide_AddHuman(t *testing.T) { name: "user invalid, invalid argument error", fields: fields{ eventstore: expectEventstore(), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -123,6 +125,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), ), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -155,6 +158,7 @@ func TestCommandSide_AddHuman(t *testing.T) { expectFilter(), expectFilter(), ), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -196,6 +200,7 @@ func TestCommandSide_AddHuman(t *testing.T) { expectFilter(), expectFilter(), ), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -264,6 +269,7 @@ func TestCommandSide_AddHuman(t *testing.T) { idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), newCode: mockEncryptedCode("userinit", time.Hour), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -333,6 +339,7 @@ func TestCommandSide_AddHuman(t *testing.T) { idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), newCode: mockEncryptedCode("userinit", time.Hour), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -403,6 +410,7 @@ func TestCommandSide_AddHuman(t *testing.T) { idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), newCode: mockEncryptedCode("userinit", time.Hour), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -473,6 +481,7 @@ func TestCommandSide_AddHuman(t *testing.T) { idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), newCode: mockEncryptedCode("userinit", time.Hour), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -541,11 +550,11 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - userPasswordHasher: mockPasswordHasher("x"), - codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), - newCode: mockEncryptedCode("userinit", time.Hour), - defaultEmailCodeURLTemplate: func(_ context.Context) string { return "" }, + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + newCode: mockEncryptedCode("userinit", time.Hour), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -600,7 +609,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), expectPush( newAddHumanEvent("$plain$x$password", false, true, "", AllowedLanguage), - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -619,6 +628,7 @@ func TestCommandSide_AddHuman(t *testing.T) { userPasswordHasher: mockPasswordHasher("x"), codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), newCode: mockEncryptedCode("emailCode", time.Hour), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -674,7 +684,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), expectPush( newAddHumanEvent("$plain$x$password", false, true, "", AllowedLanguage), - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -689,11 +699,11 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - userPasswordHasher: mockPasswordHasher("x"), - codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), - newCode: mockEncryptedCode("emailCode", time.Hour), - defaultEmailCodeURLTemplate: func(_ context.Context) string { return "http://example.com/{{.user}}/email/{{.code}}" }, + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + newCode: mockEncryptedCode("emailCode", time.Hour), + loginPaths: expectLoginPathsDefaultEmailCodeURLTemplate("http://example.com/{{.user}}/email/{{.code}}"), }, args: args{ ctx: context.Background(), @@ -755,6 +765,7 @@ func TestCommandSide_AddHuman(t *testing.T) { idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), userPasswordHasher: mockPasswordHasher("x"), codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -819,6 +830,7 @@ func TestCommandSide_AddHuman(t *testing.T) { idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), userPasswordHasher: mockPasswordHasher("x"), codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -883,6 +895,7 @@ func TestCommandSide_AddHuman(t *testing.T) { idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), userPasswordHasher: mockPasswordHasher("x"), codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -947,6 +960,7 @@ func TestCommandSide_AddHuman(t *testing.T) { idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), userPasswordHasher: mockPasswordHasher("x"), codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -1000,6 +1014,7 @@ func TestCommandSide_AddHuman(t *testing.T) { idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), userPasswordHasher: mockPasswordHasher("x"), codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -1086,6 +1101,7 @@ func TestCommandSide_AddHuman(t *testing.T) { idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), userPasswordHasher: mockPasswordHasher("x"), codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -1195,6 +1211,7 @@ func TestCommandSide_AddHuman(t *testing.T) { codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phonecode", time.Hour), defaultSecretGenerators: defaultGenerators, + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -1262,6 +1279,7 @@ func TestCommandSide_AddHuman(t *testing.T) { idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), newCode: mockEncryptedCode("userinit", time.Hour), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -1371,6 +1389,7 @@ func TestCommandSide_AddHuman(t *testing.T) { codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneCode", time.Hour), defaultSecretGenerators: defaultGenerators, + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -1441,6 +1460,7 @@ func TestCommandSide_AddHuman(t *testing.T) { idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), newCode: mockEncryptedCode("userinit", time.Hour), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -1481,7 +1501,7 @@ func TestCommandSide_AddHuman(t *testing.T) { newEncryptedCode: tt.fields.newCode, newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, defaultSecretGenerators: tt.fields.defaultSecretGenerators, - defaultEmailCodeURLTemplate: tt.fields.defaultEmailCodeURLTemplate, + loginPaths: tt.fields.loginPaths(t), } err := r.AddHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.allowInitMail) if tt.res.err == nil { diff --git a/internal/command/user_v2_email.go b/internal/command/user_v2_email.go index 59793e50b..1a05fe9d7 100644 --- a/internal/command/user_v2_email.go +++ b/internal/command/user_v2_email.go @@ -156,7 +156,7 @@ func (c *Commands) changeUserEmailWithGeneratorEvents(ctx context.Context, userI } if urlTmpl == "" { - urlTmpl = c.defaultEmailCodeURLTemplate(ctx) + urlTmpl = c.loginPaths.DefaultEmailCodeURLTemplate(ctx) } if err = cmd.AddGeneratedCode(ctx, gen, urlTmpl, returnCode); err != nil { @@ -178,7 +178,7 @@ func (c *Commands) sendUserEmailCodeWithGeneratorEvents(ctx context.Context, use } if urlTmpl == "" { - urlTmpl = c.defaultEmailCodeURLTemplate(ctx) + urlTmpl = c.loginPaths.DefaultEmailCodeURLTemplate(ctx) } if err = cmd.AddGeneratedCode(ctx, gen, urlTmpl, returnCode); err != nil { @@ -286,7 +286,7 @@ func generateCodeCommand(ctx context.Context, agg *eventstore.Aggregate, gen cry return nil, "", err } - cmd := user.NewHumanEmailCodeAddedEventV2(ctx, agg, value, gen.Expiry(), urlTmpl, returnCode, "") + cmd := user.NewHumanEmailCodeAddedEvent(ctx, agg, value, gen.Expiry(), urlTmpl, returnCode, "") if returnCode { return cmd, plain, nil } diff --git a/internal/command/user_v2_email_test.go b/internal/command/user_v2_email_test.go index 9bf690dc1..e4e77b072 100644 --- a/internal/command/user_v2_email_test.go +++ b/internal/command/user_v2_email_test.go @@ -439,7 +439,7 @@ func TestCommands_ResendUserEmailCode(t *testing.T) { ), ), eventFromEventPusher( - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -556,7 +556,7 @@ func TestCommands_SendUserEmailCode(t *testing.T) { ), ), eventFromEventPusher( - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -647,7 +647,7 @@ func TestCommands_ResendUserEmailCodeURLTemplate(t *testing.T) { ), ), eventFromEventPusher( - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -778,7 +778,7 @@ func TestCommands_SendUserEmailCodeURLTemplate(t *testing.T) { ), ), eventFromEventPusher( - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -858,7 +858,7 @@ func TestCommands_ResendUserEmailReturnCode(t *testing.T) { ), ), eventFromEventPusher( - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -975,7 +975,7 @@ func TestCommands_SendUserEmailReturnCode(t *testing.T) { ), ), eventFromEventPusher( - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -1160,9 +1160,9 @@ func TestCommands_ChangeUserEmailVerified(t *testing.T) { func TestCommands_changeUserEmailWithGenerator(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - checkPermission domain.PermissionCheck - defaultEmailCodeURLTemplate func(ctx context.Context) string + eventstore func(*testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + loginPaths func(*testing.T) LoginPaths } type args struct { userID string @@ -1180,7 +1180,8 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { { name: "missing user", fields: fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), + loginPaths: expectLoginPathsNoCall, }, args: args{ userID: "", @@ -1193,8 +1194,7 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { { name: "missing permission", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -1213,6 +1213,7 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { ), ), checkPermission: newMockPermissionCheckNotAllowed(), + loginPaths: expectLoginPathsNoCall, }, args: args{ userID: "user1", @@ -1225,8 +1226,7 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { { name: "missing email", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -1245,6 +1245,7 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { ), ), checkPermission: newMockPermissionCheckAllowed(), + loginPaths: expectLoginPathsNoCall, }, args: args{ userID: "user1", @@ -1257,8 +1258,7 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { { name: "not changed", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -1277,6 +1277,7 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { ), ), checkPermission: newMockPermissionCheckAllowed(), + loginPaths: expectLoginPathsNoCall, }, args: args{ userID: "user1", @@ -1289,8 +1290,7 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { { name: "email changed", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -1312,7 +1312,7 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { &user.NewAggregate("user1", "org1").Aggregate, "email-changed@test.ch", ), - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -1326,8 +1326,8 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { ), ), ), - checkPermission: newMockPermissionCheckAllowed(), - defaultEmailCodeURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/email/{{.code}}" }, + checkPermission: newMockPermissionCheckAllowed(), + loginPaths: expectLoginPathsDefaultEmailCodeURLTemplate("http://example.com/{{.user}}/email/{{.code}}"), }, args: args{ userID: "user1", @@ -1347,8 +1347,7 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { { name: "email changed, return code", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -1370,7 +1369,7 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { &user.NewAggregate("user1", "org1").Aggregate, "email-changed@test.ch", ), - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -1383,8 +1382,8 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { ), ), ), - checkPermission: newMockPermissionCheckAllowed(), - defaultEmailCodeURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/email/{{.code2}}" }, + checkPermission: newMockPermissionCheckAllowed(), + loginPaths: expectLoginPathsDefaultEmailCodeURLTemplate("http://example.com/{{.user}}/email/{{.code2}}"), }, args: args{ userID: "user1", @@ -1405,8 +1404,7 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { { name: "email changed, URL template", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -1428,7 +1426,7 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { &user.NewAggregate("user1", "org1").Aggregate, "email-changed@test.ch", ), - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -1442,6 +1440,7 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { ), ), checkPermission: newMockPermissionCheckAllowed(), + loginPaths: expectLoginPathsNoCall, }, args: args{ userID: "user1", @@ -1462,9 +1461,9 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, - checkPermission: tt.fields.checkPermission, - defaultEmailCodeURLTemplate: tt.fields.defaultEmailCodeURLTemplate, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + loginPaths: tt.fields.loginPaths(t), } got, err := c.changeUserEmailWithGenerator(context.Background(), tt.args.userID, tt.args.email, GetMockSecretGenerator(t), tt.args.returnCode, tt.args.urlTmpl) require.ErrorIs(t, tt.wantErr, err) @@ -1475,9 +1474,9 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { func TestCommands_sendUserEmailCodeWithGeneratorEvents(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - checkPermission domain.PermissionCheck - defaultEmailCodeURLTemplate func(ctx context.Context) string + eventstore *eventstore.Eventstore + checkPermission domain.PermissionCheck + loginPaths func(*testing.T) LoginPaths } type args struct { userID string @@ -1496,6 +1495,7 @@ func TestCommands_sendUserEmailCodeWithGeneratorEvents(t *testing.T) { name: "missing user", fields: fields{ eventstore: eventstoreExpect(t), + loginPaths: expectLoginPathsNoCall, }, args: args{ userID: "", @@ -1525,7 +1525,7 @@ func TestCommands_sendUserEmailCodeWithGeneratorEvents(t *testing.T) { ), ), eventFromEventPusher( - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -1540,6 +1540,7 @@ func TestCommands_sendUserEmailCodeWithGeneratorEvents(t *testing.T) { ), ), checkPermission: newMockPermissionCheckNotAllowed(), + loginPaths: expectLoginPathsNoCall, }, args: args{ userID: "user1", @@ -1570,7 +1571,7 @@ func TestCommands_sendUserEmailCodeWithGeneratorEvents(t *testing.T) { ), ), expectPush( - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -1583,8 +1584,8 @@ func TestCommands_sendUserEmailCodeWithGeneratorEvents(t *testing.T) { ), ), ), - checkPermission: newMockPermissionCheckAllowed(), - defaultEmailCodeURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/email/{{.code}}" }, + checkPermission: newMockPermissionCheckAllowed(), + loginPaths: expectLoginPathsDefaultEmailCodeURLTemplate("http://example.com/{{.user}}/email/{{.code}}"), }, args: args{ userID: "user1", @@ -1622,7 +1623,7 @@ func TestCommands_sendUserEmailCodeWithGeneratorEvents(t *testing.T) { ), ), eventFromEventPusher( - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -1636,7 +1637,7 @@ func TestCommands_sendUserEmailCodeWithGeneratorEvents(t *testing.T) { ), ), expectPush( - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -1649,8 +1650,8 @@ func TestCommands_sendUserEmailCodeWithGeneratorEvents(t *testing.T) { ), ), ), - checkPermission: newMockPermissionCheckAllowed(), - defaultEmailCodeURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/email/{{.code2}}" }, + checkPermission: newMockPermissionCheckAllowed(), + loginPaths: expectLoginPathsDefaultEmailCodeURLTemplate("http://example.com/{{.user}}/email/{{.code2}}"), }, args: args{ userID: "user1", @@ -1690,6 +1691,7 @@ func TestCommands_sendUserEmailCodeWithGeneratorEvents(t *testing.T) { ), ), checkPermission: newMockPermissionCheckAllowed(), + loginPaths: expectLoginPathsNoCall, }, args: args{ userID: "user1", @@ -1721,7 +1723,7 @@ func TestCommands_sendUserEmailCodeWithGeneratorEvents(t *testing.T) { ), ), expectPush( - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -1734,8 +1736,8 @@ func TestCommands_sendUserEmailCodeWithGeneratorEvents(t *testing.T) { ), ), ), - checkPermission: newMockPermissionCheckAllowed(), - defaultEmailCodeURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/email/{{.code}}" }, + checkPermission: newMockPermissionCheckAllowed(), + loginPaths: expectLoginPathsDefaultEmailCodeURLTemplate("http://example.com/{{.user}}/email/{{.code}}"), }, args: args{ userID: "user1", @@ -1775,7 +1777,7 @@ func TestCommands_sendUserEmailCodeWithGeneratorEvents(t *testing.T) { ), ), expectPush( - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -1789,6 +1791,7 @@ func TestCommands_sendUserEmailCodeWithGeneratorEvents(t *testing.T) { ), ), checkPermission: newMockPermissionCheckAllowed(), + loginPaths: expectLoginPathsNoCall, }, args: args{ userID: "user1", @@ -1809,9 +1812,9 @@ func TestCommands_sendUserEmailCodeWithGeneratorEvents(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, - checkPermission: tt.fields.checkPermission, - defaultEmailCodeURLTemplate: tt.fields.defaultEmailCodeURLTemplate, + eventstore: tt.fields.eventstore, + checkPermission: tt.fields.checkPermission, + loginPaths: tt.fields.loginPaths(t), } got, err := c.sendUserEmailCodeWithGenerator(context.Background(), tt.args.userID, GetMockSecretGenerator(t), tt.args.returnCode, tt.args.urlTmpl, tt.args.checkExisting) require.ErrorIs(t, err, tt.wantErr) @@ -1924,7 +1927,7 @@ func TestCommands_VerifyUserEmail(t *testing.T) { ), ), eventFromEventPusher( - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -2039,7 +2042,7 @@ func TestCommands_verifyUserEmailWithGenerator(t *testing.T) { ), ), eventFromEventPusher( - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -2086,7 +2089,7 @@ func TestCommands_verifyUserEmailWithGenerator(t *testing.T) { ), ), eventFromEventPusherWithCreationDateNow( - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, diff --git a/internal/command/user_v2_human.go b/internal/command/user_v2_human.go index 578f17e09..60277a017 100644 --- a/internal/command/user_v2_human.go +++ b/internal/command/user_v2_human.go @@ -413,10 +413,10 @@ func (c *Commands) changeUserEmail(ctx context.Context, cmds []eventstore.Comman return cmds, code, err } if email.URLTemplate == "" { - email.URLTemplate = c.defaultEmailCodeURLTemplate(ctx) + email.URLTemplate = c.loginPaths.DefaultEmailCodeURLTemplate(ctx) } - cmds = append(cmds, user.NewHumanEmailCodeAddedEventV2(ctx, &wm.Aggregate().Aggregate, cryptoCode.Crypted, cryptoCode.Expiry, email.URLTemplate, email.ReturnCode, "")) + cmds = append(cmds, user.NewHumanEmailCodeAddedEvent(ctx, &wm.Aggregate().Aggregate, cryptoCode.Crypted, cryptoCode.Expiry, email.URLTemplate, email.ReturnCode, "")) if email.ReturnCode { code = &cryptoCode.Plain } diff --git a/internal/command/user_v2_human_test.go b/internal/command/user_v2_human_test.go index 44884c26d..86a22cff4 100644 --- a/internal/command/user_v2_human_test.go +++ b/internal/command/user_v2_human_test.go @@ -43,7 +43,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc checkPermission domain.PermissionCheck defaultSecretGenerators *SecretGenerators - defaultEmailCodeURLTemplate func(ctx context.Context) string + loginPaths func(*testing.T) LoginPaths } type args struct { ctx context.Context @@ -79,6 +79,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { fields: fields{ eventstore: expectEventstore(), checkPermission: newMockPermissionCheckAllowed(), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -104,6 +105,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { fields: fields{ eventstore: expectEventstore(), checkPermission: newMockPermissionCheckAllowed(), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -131,6 +133,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { ), ), checkPermission: newMockPermissionCheckAllowed(), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -163,6 +166,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { expectFilter(), ), checkPermission: newMockPermissionCheckAllowed(), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -205,6 +209,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { expectFilter(), ), checkPermission: newMockPermissionCheckAllowed(), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -274,6 +279,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), newCode: mockEncryptedCode("userinit", time.Hour), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -310,6 +316,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckNotAllowed(), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), newCode: mockEncryptedCode("userinit", time.Hour), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -376,6 +383,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), newCode: mockEncryptedCode("userinit", time.Hour), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -449,6 +457,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), userPasswordHasher: mockPasswordHasher("x"), newCode: mockEncryptedCode("userinit", time.Hour), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -492,7 +501,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { expectFilterOrganizationSettings("org1", false, false), expectPush( newAddHumanEvent("", false, true, "", language.English), - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -507,10 +516,10 @@ func TestCommandSide_AddUserHuman(t *testing.T) { ), ), ), - checkPermission: newMockPermissionCheckAllowed(), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - newCode: mockEncryptedCode("emailverify", time.Hour), - defaultEmailCodeURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/email/{{.code}}" }, + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + newCode: mockEncryptedCode("emailverify", time.Hour), + loginPaths: expectLoginPathsDefaultEmailCodeURLTemplate("http://example.com/{{.user}}/email/{{.code}}"), }, args: args{ ctx: context.Background(), @@ -565,7 +574,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { ), expectPush( newAddHumanEvent("$plain$x$password", false, true, "", language.English), - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -584,6 +593,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), userPasswordHasher: mockPasswordHasher("x"), newCode: mockEncryptedCode("emailCode", time.Hour), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -640,7 +650,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { ), expectPush( newAddHumanEvent("$plain$x$password", false, true, "", language.English), - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -655,11 +665,11 @@ func TestCommandSide_AddUserHuman(t *testing.T) { ), ), ), - checkPermission: newMockPermissionCheckAllowed(), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - userPasswordHasher: mockPasswordHasher("x"), - newCode: mockEncryptedCode("emailCode", time.Hour), - defaultEmailCodeURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/email/{{.code}}" }, + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + newCode: mockEncryptedCode("emailCode", time.Hour), + loginPaths: expectLoginPathsDefaultEmailCodeURLTemplate("http://example.com/{{.user}}/email/{{.code}}"), }, args: args{ ctx: context.Background(), @@ -725,6 +735,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), userPasswordHasher: mockPasswordHasher("x"), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -790,6 +801,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), userPasswordHasher: mockPasswordHasher("x"), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -855,6 +867,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), userPasswordHasher: mockPasswordHasher("x"), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -909,6 +922,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), userPasswordHasher: mockPasswordHasher("x"), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -996,6 +1010,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), userPasswordHasher: mockPasswordHasher("x"), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -1106,6 +1121,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { userPasswordHasher: mockPasswordHasher("x"), newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phonecode", time.Hour), defaultSecretGenerators: defaultGenerators, + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -1212,6 +1228,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { userPasswordHasher: mockPasswordHasher("x"), newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phonecode", time.Hour), defaultSecretGenerators: defaultGenerators, + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -1280,6 +1297,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), newCode: mockEncryptedCode("userinit", time.Hour), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -1390,6 +1408,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { userPasswordHasher: mockPasswordHasher("x"), newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneCode", time.Hour), defaultSecretGenerators: defaultGenerators, + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -1461,6 +1480,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), newCode: mockEncryptedCode("userinit", time.Hour), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -1522,7 +1542,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { ), expectPush( newRegisterHumanEvent("email@test.ch", "", false, true, "", language.English), - user.NewHumanEmailCodeAddedEventV2( + user.NewHumanEmailCodeAddedEvent( context.Background(), &userAgg.Aggregate, &crypto.CryptoValue{ @@ -1545,10 +1565,10 @@ func TestCommandSide_AddUserHuman(t *testing.T) { ), ), ), - checkPermission: newMockPermissionCheckAllowed(), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - newCode: mockEncryptedCode("mailVerify", time.Hour), - defaultEmailCodeURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/email/{{.code}}" }, + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + newCode: mockEncryptedCode("mailVerify", time.Hour), + loginPaths: expectLoginPathsDefaultEmailCodeURLTemplate("http://example.com/{{.user}}/email/{{.code}}"), }, args: args{ ctx: context.Background(), @@ -1629,6 +1649,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), newCode: mockEncryptedCode("mailVerify", time.Hour), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -1717,6 +1738,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), newCode: mockEncryptedCode("userinit", time.Hour), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -1801,6 +1823,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), newCode: mockEncryptedCode("userinit", time.Hour), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -1858,6 +1881,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), newCode: mockEncryptedCode("userinit", time.Hour), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -1938,6 +1962,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), newCode: mockEncryptedCode("userinit", time.Hour), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -2028,6 +2053,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), newCode: mockEncryptedCode("userinit", time.Hour), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -2122,6 +2148,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), newCode: mockEncryptedCode("userinit", time.Hour), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -2168,7 +2195,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { CryptoMFA: cryptoAlg, }, }, - defaultEmailCodeURLTemplate: tt.fields.defaultEmailCodeURLTemplate, + loginPaths: tt.fields.loginPaths(t), } err := r.AddUserHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.allowInitMail, tt.args.codeAlg) if tt.res.err == nil { @@ -2206,7 +2233,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc checkPermission domain.PermissionCheck defaultSecretGenerators *SecretGenerators - defaultEmailCodeURLTemplate func(ctx context.Context) string + loginPaths func(*testing.T) LoginPaths tarpit Tarpit } type args struct { @@ -2245,6 +2272,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { ), checkPermission: newMockPermissionCheckAllowed(), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -2271,6 +2299,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { ), checkPermission: newMockPermissionCheckNotAllowed(), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -2293,6 +2322,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { ), checkPermission: newMockPermissionCheckAllowed(), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -2339,6 +2369,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { ), checkPermission: newMockPermissionCheckAllowed(), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -2387,6 +2418,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { ), checkPermission: newMockPermissionCheckAllowed(), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -2415,6 +2447,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { ), checkPermission: newMockPermissionCheckAllowed(), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -2443,6 +2476,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { ), checkPermission: newMockPermissionCheckNotAllowed(), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -2492,6 +2526,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { ), checkPermission: newMockPermissionCheckAllowed(), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -2527,6 +2562,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { ), checkPermission: newMockPermissionCheckAllowed(), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -2564,7 +2600,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { &userAgg.Aggregate, "changed@example.com", ), - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -2579,10 +2615,10 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { ), ), ), - checkPermission: newMockPermissionCheckAllowed(), - newCode: mockEncryptedCode("emailCode", time.Hour), - defaultEmailCodeURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/email/{{.code}}" }, - tarpit: expectTarpit(0), + checkPermission: newMockPermissionCheckAllowed(), + newCode: mockEncryptedCode("emailCode", time.Hour), + loginPaths: expectLoginPathsDefaultEmailCodeURLTemplate("http://example.com/{{.user}}/email/{{.code}}"), + tarpit: expectTarpit(0), }, args: args{ ctx: context.Background(), @@ -2614,6 +2650,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { ), checkPermission: newMockPermissionCheckAllowed(), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -2644,6 +2681,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { ), checkPermission: newMockPermissionCheckNotAllowed(), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -2682,6 +2720,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { ), checkPermission: newMockPermissionCheckAllowed(), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -2718,6 +2757,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { ), checkPermission: newMockPermissionCheckAllowed(), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -2750,7 +2790,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { &userAgg.Aggregate, "changed@test.com", ), - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -2765,10 +2805,10 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { ), ), ), - checkPermission: newMockPermissionCheckAllowed(), - newCode: mockEncryptedCode("emailCode", time.Hour), - defaultEmailCodeURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/email/{{.code}}" }, - tarpit: expectTarpit(0), + checkPermission: newMockPermissionCheckAllowed(), + newCode: mockEncryptedCode("emailCode", time.Hour), + loginPaths: expectLoginPathsDefaultEmailCodeURLTemplate("http://example.com/{{.user}}/email/{{.code}}"), + tarpit: expectTarpit(0), }, args: args{ ctx: context.Background(), @@ -2852,6 +2892,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneCode", time.Hour), defaultSecretGenerators: defaultGenerators, tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -2928,6 +2969,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneCode", time.Hour), defaultSecretGenerators: defaultGenerators, tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -2959,6 +3001,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { ), checkPermission: newMockPermissionCheckNotAllowed(), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -2997,6 +3040,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { ), checkPermission: newMockPermissionCheckAllowed(), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -3033,6 +3077,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { ), checkPermission: newMockPermissionCheckAllowed(), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -3113,6 +3158,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneCode", time.Hour), defaultSecretGenerators: defaultGenerators, tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -3141,6 +3187,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), userPasswordHasher: mockPasswordHasher("x"), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -3175,6 +3222,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), userPasswordHasher: mockPasswordHasher("x"), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -3224,6 +3272,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), userPasswordHasher: mockPasswordHasher("x"), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -3249,6 +3298,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), userPasswordHasher: mockPasswordHasher("x"), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -3284,6 +3334,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { userPasswordHasher: mockPasswordHasher("x"), checkPermission: newMockPermissionCheckNotAllowed(), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -3339,6 +3390,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { userPasswordHasher: mockPasswordHasher("x"), checkPermission: newMockPermissionCheckAllowed(), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -3397,6 +3449,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), userPasswordHasher: mockPasswordHasher("x"), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -3452,6 +3505,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), userPasswordHasher: mockPasswordHasher("x"), tarpit: expectTarpit(1), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -3524,6 +3578,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), userPasswordHasher: mockPasswordHasher("x"), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -3579,6 +3634,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), userPasswordHasher: mockPasswordHasher("x"), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -3644,6 +3700,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), userPasswordHasher: mockPasswordHasher("x"), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -3703,6 +3760,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), userPasswordHasher: mockPasswordHasher("x"), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -3778,6 +3836,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), userPasswordHasher: mockPasswordHasher("x"), tarpit: expectTarpit(0), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -3810,7 +3869,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { checkPermission: tt.fields.checkPermission, defaultSecretGenerators: tt.fields.defaultSecretGenerators, userEncryption: tt.args.codeAlg, - defaultEmailCodeURLTemplate: tt.fields.defaultEmailCodeURLTemplate, + loginPaths: tt.fields.loginPaths(t), tarpit: tt.fields.tarpit.tarpit, } err := r.ChangeUserHuman(tt.args.ctx, tt.args.human, tt.args.codeAlg) diff --git a/internal/command/user_v2_model_test.go b/internal/command/user_v2_model_test.go index ecaa2db40..602553cef 100644 --- a/internal/command/user_v2_model_test.go +++ b/internal/command/user_v2_model_test.go @@ -662,7 +662,7 @@ func TestCommandSide_userHumanWriteModel_email(t *testing.T) { newRegisterHumanEvent("username", "$plain$x$password", true, true, "", language.English), ), eventFromEventPusher( - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &userAgg.Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -724,7 +724,7 @@ func TestCommandSide_userHumanWriteModel_email(t *testing.T) { newRegisterHumanEvent("username", "$plain$x$password", true, true, "", language.English), ), eventFromEventPusher( - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &userAgg.Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -783,7 +783,7 @@ func TestCommandSide_userHumanWriteModel_email(t *testing.T) { newRegisterHumanEvent("username", "$plain$x$password", true, true, "", language.English), ), eventFromEventPusher( - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &userAgg.Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -851,7 +851,7 @@ func TestCommandSide_userHumanWriteModel_email(t *testing.T) { newRegisterHumanEvent("username", "$plain$x$password", true, true, "", language.English), ), eventFromEventPusher( - user.NewHumanEmailCodeAddedEventV2(context.Background(), + user.NewHumanEmailCodeAddedEvent(context.Background(), &userAgg.Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, diff --git a/internal/command/user_v2_passkey.go b/internal/command/user_v2_passkey.go index a38604974..0f88b8837 100644 --- a/internal/command/user_v2_passkey.go +++ b/internal/command/user_v2_passkey.go @@ -143,6 +143,10 @@ func (c *Commands) addUserPasskeyCode(ctx context.Context, userID, resourceOwner } agg := UserAggregateFromWriteModel(&wm.WriteModel) + if urlTmpl == "" { + urlTmpl = c.loginPaths.DefaultPasskeySetURLTemplate(ctx) + } + cmd := user.NewHumanPasswordlessInitCodeRequestedEvent(ctx, agg, codeID, code.Crypted, code.Expiry, urlTmpl, returnCode) err = c.pushAppendAndReduce(ctx, wm, cmd) if err != nil { diff --git a/internal/command/user_v2_passkey_test.go b/internal/command/user_v2_passkey_test.go index 0d1009862..1609f1ab4 100644 --- a/internal/command/user_v2_passkey_test.go +++ b/internal/command/user_v2_passkey_test.go @@ -142,9 +142,9 @@ func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) { ExternalSecure: true, } alg := crypto.CreateMockEncryptionAlg(gomock.NewController(t)) - es := eventstoreExpect(t, + es := expectEventstore( expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))), - ) + )(t) code, err := newEncryptedCode(ctx, es.Filter, domain.SecretGeneratorTypePasswordlessInitCode, alg) //nolint:staticcheck require.NoError(t, err) userAgg := &user.NewAggregate("user1", "org1").Aggregate @@ -238,9 +238,9 @@ func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) { func TestCommands_verifyUserPasskeyCode(t *testing.T) { ctx := http_util.WithRequestedHost(context.Background(), "example.com") alg := crypto.CreateMockEncryptionAlg(gomock.NewController(t)) - es := eventstoreExpect(t, + es := expectEventstore( expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))), - ) + )(t) code, err := newEncryptedCode(ctx, es.Filter, domain.SecretGeneratorTypePasswordlessInitCode, alg) //nolint:staticcheck require.NoError(t, err) userAgg := &user.NewAggregate("user1", "org1").Aggregate @@ -436,14 +436,14 @@ func TestCommands_pushUserPasskey(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: eventstoreExpect(t, prep...), + eventstore: expectEventstore(prep...)(t), webauthnConfig: webauthnConfig, idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"), } wm, userAgg, webAuthN, err := c.createUserPasskey(ctx, "user1", "org1", "rpID", domain.AuthenticatorAttachmentCrossPlattform) require.NoError(t, err) - c.eventstore = eventstoreExpect(t, tt.expectPush(webAuthN.Challenge)) + c.eventstore = expectEventstore(tt.expectPush(webAuthN.Challenge))(t) got, err := c.pushUserPasskey(ctx, wm, userAgg, webAuthN, tt.args.events...) require.ErrorIs(t, err, tt.wantErr) @@ -463,6 +463,7 @@ func TestCommands_AddUserPasskeyCode(t *testing.T) { newCode encrypedCodeFunc eventstore func(t *testing.T) *eventstore.Eventstore idGenerator id.Generator + loginPaths func(*testing.T) LoginPaths } type args struct { userID string @@ -481,6 +482,7 @@ func TestCommands_AddUserPasskeyCode(t *testing.T) { newCode: mockEncryptedCode("passkey1", time.Hour), eventstore: expectEventstore(), idGenerator: id_mock.NewIDGeneratorExpectError(t, io.ErrClosedPipe), + loginPaths: expectLoginPathsNoCall, }, args: args{ userID: "user1", @@ -519,6 +521,49 @@ func TestCommands_AddUserPasskeyCode(t *testing.T) { ), ), ), + loginPaths: expectLoginPathsDefaultPasskeySetURLTemplate(""), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + }, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + { + name: "success, url template", + fields: fields{ + newCode: mockEncryptedCode("passkey1", time.Minute), + eventstore: expectEventstore( + expectFilter(eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + userAgg, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + )), + expectPush( + user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(), + userAgg, + "123", &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("passkey1"), + }, time.Minute, "http://example.com/{{.user}}/email/{{.code}}", false, + ), + ), + ), + loginPaths: expectLoginPathsDefaultPasskeySetURLTemplate("http://example.com/{{.user}}/email/{{.code}}"), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"), }, args: args{ @@ -536,6 +581,7 @@ func TestCommands_AddUserPasskeyCode(t *testing.T) { newEncryptedCode: tt.fields.newCode, eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, + loginPaths: tt.fields.loginPaths(t), } got, err := c.AddUserPasskeyCode(context.Background(), tt.args.userID, tt.args.resourceOwner, alg) require.ErrorIs(t, err, tt.wantErr) @@ -550,7 +596,7 @@ func TestCommands_AddUserPasskeyCodeURLTemplate(t *testing.T) { type fields struct { newCode encrypedCodeFunc - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore idGenerator id.Generator } type args struct { @@ -569,7 +615,7 @@ func TestCommands_AddUserPasskeyCodeURLTemplate(t *testing.T) { name: "template error", fields: fields{ newCode: newEncryptedCode, - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args: args{ userID: "user1", @@ -582,7 +628,7 @@ func TestCommands_AddUserPasskeyCodeURLTemplate(t *testing.T) { name: "id generator error", fields: fields{ newCode: newEncryptedCode, - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), idGenerator: id_mock.NewIDGeneratorExpectError(t, io.ErrClosedPipe), }, args: args{ @@ -596,7 +642,7 @@ func TestCommands_AddUserPasskeyCodeURLTemplate(t *testing.T) { name: "success", fields: fields{ newCode: mockEncryptedCode("passkey1", time.Minute), - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), userAgg, @@ -642,7 +688,7 @@ func TestCommands_AddUserPasskeyCodeURLTemplate(t *testing.T) { t.Run(tt.name, func(t *testing.T) { c := &Commands{ newEncryptedCode: tt.fields.newCode, - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, } got, err := c.AddUserPasskeyCodeURLTemplate(context.Background(), tt.args.userID, tt.args.resourceOwner, alg, tt.args.urlTmpl) @@ -657,8 +703,9 @@ func TestCommands_AddUserPasskeyCodeReturn(t *testing.T) { userAgg := &user.NewAggregate("user1", "org1").Aggregate type fields struct { newCode encrypedCodeFunc - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore idGenerator id.Generator + loginPaths func(*testing.T) LoginPaths } type args struct { userID string @@ -675,8 +722,9 @@ func TestCommands_AddUserPasskeyCodeReturn(t *testing.T) { name: "id generator error", fields: fields{ newCode: newEncryptedCode, - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), idGenerator: id_mock.NewIDGeneratorExpectError(t, io.ErrClosedPipe), + loginPaths: expectLoginPathsNoCall, }, args: args{ userID: "user1", @@ -688,7 +736,7 @@ func TestCommands_AddUserPasskeyCodeReturn(t *testing.T) { name: "success", fields: fields{ newCode: mockEncryptedCode("passkey1", time.Minute), - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), userAgg, @@ -716,6 +764,53 @@ func TestCommands_AddUserPasskeyCodeReturn(t *testing.T) { ), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"), + loginPaths: expectLoginPathsDefaultPasskeySetURLTemplate(""), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + }, + want: &domain.PasskeyCodeDetails{ + ObjectDetails: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + CodeID: "123", + Code: "passkey1", + }, + }, + { + name: "success, url template", + fields: fields{ + newCode: mockEncryptedCode("passkey1", time.Minute), + eventstore: expectEventstore( + expectFilter(eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + userAgg, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + )), + expectPush( + user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(), + userAgg, + "123", &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("passkey1"), + }, time.Minute, "http://example.com/{{.user}}/email/{{.code}}", true, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"), + loginPaths: expectLoginPathsDefaultPasskeySetURLTemplate("http://example.com/{{.user}}/email/{{.code}}"), }, args: args{ userID: "user1", @@ -734,8 +829,9 @@ func TestCommands_AddUserPasskeyCodeReturn(t *testing.T) { t.Run(tt.name, func(t *testing.T) { c := &Commands{ newEncryptedCode: tt.fields.newCode, - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, + loginPaths: tt.fields.loginPaths(t), } got, err := c.AddUserPasskeyCodeReturn(context.Background(), tt.args.userID, tt.args.resourceOwner, alg) if tt.wantErr != nil { @@ -754,8 +850,9 @@ func TestCommands_addUserPasskeyCode(t *testing.T) { userAgg := &user.NewAggregate("user1", "org1").Aggregate type fields struct { newCode encrypedCodeFunc - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore idGenerator id.Generator + loginPaths func(*testing.T) LoginPaths } type args struct { userID string @@ -772,8 +869,9 @@ func TestCommands_addUserPasskeyCode(t *testing.T) { name: "id generator error", fields: fields{ newCode: newEncryptedCode, - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), idGenerator: id_mock.NewIDGeneratorExpectError(t, io.ErrClosedPipe), + loginPaths: expectLoginPathsNoCall, }, args: args{ userID: "user1", @@ -785,8 +883,9 @@ func TestCommands_addUserPasskeyCode(t *testing.T) { name: "crypto error", fields: fields{ newCode: newEncryptedCode, - eventstore: eventstoreExpect(t, expectFilterError(io.ErrClosedPipe)), + eventstore: expectEventstore(expectFilterError(io.ErrClosedPipe)), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"), + loginPaths: expectLoginPathsNoCall, }, args: args{ userID: "user1", @@ -798,11 +897,12 @@ func TestCommands_addUserPasskeyCode(t *testing.T) { name: "filter query error", fields: fields{ newCode: newEncryptedCode, - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))), expectFilterError(io.ErrClosedPipe), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"), + loginPaths: expectLoginPathsNoCall, }, args: args{ userID: "user1", @@ -814,7 +914,7 @@ func TestCommands_addUserPasskeyCode(t *testing.T) { name: "push error", fields: fields{ newCode: mockEncryptedCode("passkey1", time.Minute), - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), userAgg, @@ -842,6 +942,7 @@ func TestCommands_addUserPasskeyCode(t *testing.T) { ), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"), + loginPaths: expectLoginPathsDefaultPasskeySetURLTemplate(""), }, args: args{ userID: "user1", @@ -853,7 +954,7 @@ func TestCommands_addUserPasskeyCode(t *testing.T) { name: "success", fields: fields{ newCode: mockEncryptedCode("passkey1", time.Minute), - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), userAgg, @@ -881,6 +982,52 @@ func TestCommands_addUserPasskeyCode(t *testing.T) { ), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"), + loginPaths: expectLoginPathsDefaultPasskeySetURLTemplate(""), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + }, + want: &domain.PasskeyCodeDetails{ + ObjectDetails: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + CodeID: "123", + Code: "passkey1", + }, + }, { + name: "success, url template", + fields: fields{ + newCode: mockEncryptedCode("passkey1", time.Minute), + eventstore: expectEventstore( + expectFilter(eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + userAgg, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + )), + expectPush( + user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(), + userAgg, + "123", &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("passkey1"), + }, time.Minute, "http://example.com/{{.user}}/email/{{.code}}", false, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"), + loginPaths: expectLoginPathsDefaultPasskeySetURLTemplate("http://example.com/{{.user}}/email/{{.code}}"), }, args: args{ userID: "user1", @@ -899,8 +1046,9 @@ func TestCommands_addUserPasskeyCode(t *testing.T) { t.Run(tt.name, func(t *testing.T) { c := &Commands{ newEncryptedCode: tt.fields.newCode, - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, + loginPaths: tt.fields.loginPaths(t), } got, err := c.addUserPasskeyCode(context.Background(), tt.args.userID, tt.args.resourceOwner, alg, "", false) if tt.wantErr != nil { diff --git a/internal/command/user_v2_password.go b/internal/command/user_v2_password.go index 7961f740c..7b91871c0 100644 --- a/internal/command/user_v2_password.go +++ b/internal/command/user_v2_password.go @@ -63,7 +63,7 @@ func (c *Commands) requestPasswordReset(ctx context.Context, userID string, retu return nil, nil, err } if urlTmpl == "" { - urlTmpl = c.defaultPasswordSetURLTemplate(ctx) + urlTmpl = c.loginPaths.DefaultPasswordSetURLTemplate(ctx) } cmd := user.NewHumanPasswordCodeAddedEventV2(ctx, UserAggregateFromWriteModelCtx(ctx, &model.WriteModel), passwordCode.CryptedCode(), passwordCode.CodeExpiry(), notificationType, urlTmpl, returnCode, generatorID) diff --git a/internal/command/user_v2_password_test.go b/internal/command/user_v2_password_test.go index 23b1afa07..f2c8c3148 100644 --- a/internal/command/user_v2_password_test.go +++ b/internal/command/user_v2_password_test.go @@ -338,13 +338,13 @@ func TestCommands_requestPasswordReset(t *testing.T) { }, } type fields struct { - checkPermission domain.PermissionCheck - eventstore func(t *testing.T) *eventstore.Eventstore - userEncryption crypto.EncryptionAlgorithm - newCode encrypedCodeFunc - newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc - defaultSecretGenerators *SecretGenerators - defaultPasswordSetURLTemplate func(ctx context.Context) string + checkPermission domain.PermissionCheck + eventstore func(t *testing.T) *eventstore.Eventstore + userEncryption crypto.EncryptionAlgorithm + newCode encrypedCodeFunc + newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc + defaultSecretGenerators *SecretGenerators + loginPaths func(*testing.T) LoginPaths } type args struct { ctx context.Context @@ -368,6 +368,7 @@ func TestCommands_requestPasswordReset(t *testing.T) { name: "missing userID", fields: fields{ eventstore: expectEventstore(), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -383,6 +384,7 @@ func TestCommands_requestPasswordReset(t *testing.T) { eventstore: expectEventstore( expectFilter(), ), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -408,6 +410,7 @@ func TestCommands_requestPasswordReset(t *testing.T) { ), ), ), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -430,6 +433,7 @@ func TestCommands_requestPasswordReset(t *testing.T) { ), ), checkPermission: newMockPermissionCheckNotAllowed(), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -466,9 +470,9 @@ func TestCommands_requestPasswordReset(t *testing.T) { ), ), ), - checkPermission: newMockPermissionCheckAllowed(), - newCode: mockEncryptedCode("code", 10*time.Minute), - defaultPasswordSetURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/password/{{.code}}" }, + checkPermission: newMockPermissionCheckAllowed(), + newCode: mockEncryptedCode("code", 10*time.Minute), + loginPaths: expectLoginPathsDefaultPasswordSetURLTemplate("http://example.com/{{.user}}/password/{{.code}}"), }, args: args{ ctx: context.Background(), @@ -510,6 +514,7 @@ func TestCommands_requestPasswordReset(t *testing.T) { ), checkPermission: newMockPermissionCheckAllowed(), newCode: mockEncryptedCode("code", 10*time.Minute), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -583,6 +588,7 @@ func TestCommands_requestPasswordReset(t *testing.T) { defaultSecretGenerators: defaultGenerators, checkPermission: newMockPermissionCheckAllowed(), newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("code", 10*time.Minute), + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -652,6 +658,7 @@ func TestCommands_requestPasswordReset(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), newCode: mockEncryptedCode("code", 10*time.Minute), defaultSecretGenerators: defaultGenerators, + loginPaths: expectLoginPathsNoCall, }, args: args{ ctx: context.Background(), @@ -693,9 +700,9 @@ func TestCommands_requestPasswordReset(t *testing.T) { ), ), ), - checkPermission: newMockPermissionCheckAllowed(), - newCode: mockEncryptedCode("code", 10*time.Minute), - defaultPasswordSetURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/password/{{.code}}" }, + checkPermission: newMockPermissionCheckAllowed(), + newCode: mockEncryptedCode("code", 10*time.Minute), + loginPaths: expectLoginPathsDefaultPasswordSetURLTemplate("http://example.com/{{.user}}/password/{{.code}}"), }, args: args{ ctx: context.Background(), @@ -713,13 +720,13 @@ func TestCommands_requestPasswordReset(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - checkPermission: tt.fields.checkPermission, - eventstore: tt.fields.eventstore(t), - userEncryption: tt.fields.userEncryption, - newEncryptedCode: tt.fields.newCode, - newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, - defaultSecretGenerators: tt.fields.defaultSecretGenerators, - defaultPasswordSetURLTemplate: tt.fields.defaultPasswordSetURLTemplate, + checkPermission: tt.fields.checkPermission, + eventstore: tt.fields.eventstore(t), + userEncryption: tt.fields.userEncryption, + newEncryptedCode: tt.fields.newCode, + newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, + defaultSecretGenerators: tt.fields.defaultSecretGenerators, + loginPaths: tt.fields.loginPaths(t), } got, gotPlainCode, err := c.requestPasswordReset(tt.args.ctx, tt.args.userID, tt.args.returnCode, tt.args.urlTmpl, tt.args.notificationType) diff --git a/internal/config/hook/string_to_url.go b/internal/config/hook/string_to_url.go new file mode 100644 index 000000000..842b08ef8 --- /dev/null +++ b/internal/config/hook/string_to_url.go @@ -0,0 +1,27 @@ +package hook + +import ( + "net/url" + "reflect" + + "github.com/mitchellh/mapstructure" +) + +func StringToURLHookFunc() mapstructure.DecodeHookFuncType { + return func( + f reflect.Type, + t reflect.Type, + data interface{}, + ) (interface{}, error) { + if f.Kind() != reflect.String { + return data, nil + } + + if t != reflect.TypeOf(&url.URL{}) { + return data, nil + } + + u, err := url.Parse(data.(string)) + return u, err + } +} diff --git a/internal/notification/handlers/user_notifier.go b/internal/notification/handlers/user_notifier.go index 6880d63bf..0c9b60974 100644 --- a/internal/notification/handlers/user_notifier.go +++ b/internal/notification/handlers/user_notifier.go @@ -2,6 +2,7 @@ package handlers import ( "context" + "net/url" "time" http_util "github.com/zitadel/zitadel/internal/api/http" @@ -89,7 +90,7 @@ const ( type userNotifier struct { queries *NotificationQueries - otpEmailTmpl string + otpEmailTmpl func(origin *url.URL) string queue Queue maxAttempts uint8 @@ -101,7 +102,7 @@ func NewUserNotifier( commands Commands, queries *NotificationQueries, channels types.ChannelChains, - otpEmailTmpl string, + otpEmailTmpl func(origin *url.URL) string, workerConfig WorkerConfig, queue Queue, ) *handler.Handler { @@ -530,7 +531,7 @@ func (u *userNotifier) reduceSessionOTPEmailChallenged(event eventstore.Event) ( if err != nil { return err } - origin := http_util.DomainContext(ctx).Origin() + origin := http_util.DomainContext(ctx).OriginURL() args := otpArgs(ctx, e.Expiry) args.SessionID = e.Aggregate().ID @@ -539,7 +540,7 @@ func (u *userNotifier) reduceSessionOTPEmailChallenged(event eventstore.Event) ( Aggregate: e.Aggregate(), UserID: s.UserFactor.UserID, UserResourceOwner: s.UserFactor.ResourceOwner, - TriggeredAtOrigin: origin, + TriggeredAtOrigin: origin.String(), EventType: e.EventType, NotificationType: domain.NotificationTypeEmail, MessageType: domain.VerifyEmailOTPMessageType, @@ -555,11 +556,11 @@ func (u *userNotifier) reduceSessionOTPEmailChallenged(event eventstore.Event) ( }), nil } -func (u *userNotifier) otpEmailTemplate(origin string, e *session.OTPEmailChallengedEvent) string { +func (u *userNotifier) otpEmailTemplate(origin *url.URL, e *session.OTPEmailChallengedEvent) string { if e.URLTmpl != "" { return e.URLTmpl } - return origin + u.otpEmailTmpl + return u.otpEmailTmpl(origin) } func otpArgs(ctx context.Context, expiry time.Duration) *domain.NotificationArguments { diff --git a/internal/notification/handlers/user_notifier_legacy.go b/internal/notification/handlers/user_notifier_legacy.go index 146c60e10..7117e5698 100644 --- a/internal/notification/handlers/user_notifier_legacy.go +++ b/internal/notification/handlers/user_notifier_legacy.go @@ -3,6 +3,7 @@ package handlers import ( "context" "errors" + "net/url" "strings" "time" @@ -25,7 +26,7 @@ type userNotifierLegacy struct { commands Commands queries *NotificationQueries channels types.ChannelChains - otpEmailTmpl string + otpEmailTmpl func(origin *url.URL) string } func NewUserNotifierLegacy( @@ -34,7 +35,7 @@ func NewUserNotifierLegacy( commands Commands, queries *NotificationQueries, channels types.ChannelChains, - otpEmailTmpl string, + otpEmailTmpl func(origin *url.URL) string, ) *handler.Handler { return handler.NewHandler(ctx, &config, &userNotifierLegacy{ commands: commands, @@ -421,8 +422,8 @@ func (u *userNotifierLegacy) reduceOTPEmailCodeAdded(event eventstore.Event) (*h if e.AuthRequestInfo != nil { authRequestID = e.AuthRequestInfo.ID } - url := func(code, origin string, _ *query.NotifyUser) (string, error) { - return login.OTPLink(origin, authRequestID, code, domain.MFATypeOTPEmail), nil + url := func(code string, origin *url.URL, _ *query.NotifyUser) (string, error) { + return login.OTPLink(origin.String(), authRequestID, code, domain.MFATypeOTPEmail), nil } return u.reduceOTPEmail( e, @@ -450,11 +451,11 @@ func (u *userNotifierLegacy) reduceSessionOTPEmailChallenged(event eventstore.Ev if err != nil { return nil, err } - url := func(code, origin string, user *query.NotifyUser) (string, error) { + link := func(code string, origin *url.URL, user *query.NotifyUser) (string, error) { var buf strings.Builder - urlTmpl := origin + u.otpEmailTmpl - if e.URLTmpl != "" { - urlTmpl = e.URLTmpl + urlTmpl := e.URLTmpl + if urlTmpl == "" { + urlTmpl = u.otpEmailTmpl(origin) } if err := domain.RenderOTPEmailURLTemplate(&buf, urlTmpl, code, user.ID, user.PreferredLoginName, user.DisplayName, e.Aggregate().ID, user.PreferredLanguage); err != nil { return "", err @@ -467,7 +468,7 @@ func (u *userNotifierLegacy) reduceSessionOTPEmailChallenged(event eventstore.Ev e.Expiry, s.UserFactor.UserID, s.UserFactor.ResourceOwner, - url, + link, u.commands.OTPEmailSent, user.HumanOTPEmailCodeAddedType, user.HumanOTPEmailCodeSentType, @@ -480,7 +481,7 @@ func (u *userNotifierLegacy) reduceOTPEmail( expiry time.Duration, userID, resourceOwner string, - urlTmpl func(code, origin string, user *query.NotifyUser) (string, error), + urlTmpl func(code string, origin *url.URL, user *query.NotifyUser) (string, error), sentCommand func(ctx context.Context, userID string, resourceOwner string) (err error), eventTypes ...eventstore.EventType, ) (*handler.Statement, error) { @@ -518,12 +519,12 @@ func (u *userNotifierLegacy) reduceOTPEmail( if err != nil { return nil, err } - url, err := urlTmpl(plainCode, http_util.DomainContext(ctx).Origin(), notifyUser) + link, err := urlTmpl(plainCode, http_util.DomainContext(ctx).OriginURL(), notifyUser) if err != nil { return nil, err } notify := types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, event.Type()) - err = notify.SendOTPEmailCode(ctx, url, plainCode, expiry) + err = notify.SendOTPEmailCode(ctx, link, plainCode, expiry) if err != nil { if errors.Is(err, &channels.CancelError{}) { // if the notification was canceled, we don't want to return the error, so there is no retry @@ -577,7 +578,7 @@ func (u *userNotifierLegacy) reduceDomainClaimed(event eventstore.Event) (*handl return err } err = types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, event.Type()). - SendDomainClaimed(ctx, notifyUser, e.UserName) + SendDomainClaimed(ctx, notifyUser, e.UserName, e.URLTemplate) if err != nil { if errors.Is(err, &channels.CancelError{}) { // if the notification was canceled, we don't want to return the error, so there is no retry diff --git a/internal/notification/handlers/user_notifier_legacy_test.go b/internal/notification/handlers/user_notifier_legacy_test.go index 54a0e4d40..58cd08f54 100644 --- a/internal/notification/handlers/user_notifier_legacy_test.go +++ b/internal/notification/handlers/user_notifier_legacy_test.go @@ -3,6 +3,7 @@ package handlers import ( "database/sql" "fmt" + "net/url" "testing" "time" @@ -1972,7 +1973,9 @@ func newUserNotifierLegacy(t *testing.T, ctrl *gomock.Controller, queries *mock. smtpAlg, f.SMSTokenCrypto, ), - otpEmailTmpl: defaultOTPEmailTemplate, + otpEmailTmpl: func(origin *url.URL) string { + return origin.String() + defaultOTPEmailTemplate + }, channels: ¬ificationChannels{ Chain: *senders.ChainChannels(channel), EmailConfig: &email.Config{ diff --git a/internal/notification/handlers/user_notifier_test.go b/internal/notification/handlers/user_notifier_test.go index eae40472e..258c7839b 100644 --- a/internal/notification/handlers/user_notifier_test.go +++ b/internal/notification/handlers/user_notifier_test.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "net/url" "testing" "time" @@ -1936,7 +1937,9 @@ func newUserNotifier(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQu smtpAlg, f.SMSTokenCrypto, ), - otpEmailTmpl: defaultOTPEmailTemplate, + otpEmailTmpl: func(origin *url.URL) string { + return origin.String() + defaultOTPEmailTemplate + }, } } diff --git a/internal/notification/projections.go b/internal/notification/projections.go index 7ad8a23fb..73f6dab2a 100644 --- a/internal/notification/projections.go +++ b/internal/notification/projections.go @@ -3,6 +3,7 @@ package notification import ( "context" "fmt" + "net/url" "github.com/zitadel/logging" @@ -35,7 +36,8 @@ func Register( commands *command.Commands, queries *query.Queries, es *eventstore.Eventstore, - otpEmailTmpl, fileSystemPath string, + otpEmailTmpl func(origin *url.URL) string, + fileSystemPath string, userEncryption, smtpEncryption, smsEncryption crypto.EncryptionAlgorithm, queue *queue.Queue, ) { diff --git a/internal/notification/types/domain_claimed.go b/internal/notification/types/domain_claimed.go index 433728392..36c3c33ae 100644 --- a/internal/notification/types/domain_claimed.go +++ b/internal/notification/types/domain_claimed.go @@ -10,11 +10,31 @@ import ( "github.com/zitadel/zitadel/internal/query" ) -func (notify Notify) SendDomainClaimed(ctx context.Context, user *query.NotifyUser, username string) error { - url := login.LoginLink(http_utils.DomainContext(ctx).Origin(), user.ResourceOwner) +func (notify Notify) SendDomainClaimed(ctx context.Context, user *query.NotifyUser, username, urlTemplate string) error { index := strings.LastIndex(user.LastEmail, "@") + domainSuffix := user.LastEmail[index+1:] + var url string + if urlTemplate == "" { + url = login.LoginLink(http_utils.DomainContext(ctx).Origin(), user.ResourceOwner) + } else { + var buf strings.Builder + if err := domain.RenderURLTemplate(&buf, urlTemplate, &DomainClaimedData{ + Domain: domainSuffix, + TempUsername: username, + OrgID: user.ResourceOwner, + }); err != nil { + return err + } + url = buf.String() + } args := make(map[string]interface{}) args["TempUsername"] = username - args["Domain"] = user.LastEmail[index+1:] + args["Domain"] = domainSuffix return notify(url, args, domain.DomainClaimedMessageType, true) } + +type DomainClaimedData struct { + TempUsername string + Domain string + OrgID string +} diff --git a/internal/repository/user/human_email.go b/internal/repository/user/human_email.go index 34e490323..4caf56453 100644 --- a/internal/repository/user/human_email.go +++ b/internal/repository/user/human_email.go @@ -143,16 +143,6 @@ func (e *HumanEmailCodeAddedEvent) TriggerOrigin() string { } func NewHumanEmailCodeAddedEvent( - ctx context.Context, - aggregate *eventstore.Aggregate, - code *crypto.CryptoValue, - expiry time.Duration, - authRequestID string, -) *HumanEmailCodeAddedEvent { - return NewHumanEmailCodeAddedEventV2(ctx, aggregate, code, expiry, "", false, authRequestID) -} - -func NewHumanEmailCodeAddedEventV2( ctx context.Context, aggregate *eventstore.Aggregate, code *crypto.CryptoValue, diff --git a/internal/repository/user/user.go b/internal/repository/user/user.go index 4f798fbea..68d74cb0a 100644 --- a/internal/repository/user/user.go +++ b/internal/repository/user/user.go @@ -372,6 +372,7 @@ type DomainClaimedEvent struct { eventstore.BaseEvent `json:"-"` UserName string `json:"userName"` + URLTemplate string `json:"urlTemplate,omitempty"` TriggeredAtOrigin string `json:"triggerOrigin,omitempty"` oldUserName string orgScopedUsername bool @@ -398,6 +399,7 @@ func NewDomainClaimedEvent( userName, oldUserName string, orgScopedUsername bool, + urlTemplate string, ) *DomainClaimedEvent { return &DomainClaimedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -406,6 +408,7 @@ func NewDomainClaimedEvent( UserDomainClaimedType, ), UserName: userName, + URLTemplate: urlTemplate, oldUserName: oldUserName, orgScopedUsername: orgScopedUsername, TriggeredAtOrigin: http.DomainContext(ctx).Origin(),