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(),