diff --git a/driver/config/config.go b/driver/config/config.go index 1a4bfc6b5452..eec0a9f64e33 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -106,6 +106,7 @@ const ( ViperKeyUseLegacyShowVerificationUI = "feature_flags.legacy_continue_with_verification_ui" ViperKeyLegacyOIDCRegistrationGroup = "feature_flags.legacy_oidc_registration_node_group" ViperKeyUseLegacyRequireVerifiedLoginError = "feature_flags.legacy_require_verified_login_error" + ViperKeyDisableVerificationHookAutoInjection = "feature_flags.disable_verification_hook_auto_injection" ViperKeySessionRefreshMinTimeLeft = "session.earliest_possible_extend" ViperKeyCookieSameSite = "cookies.same_site" ViperKeyCookieDomain = "cookies.domain" @@ -737,6 +738,10 @@ func (p *Config) UseLegacyRequireVerifiedLoginError(ctx context.Context) bool { return p.GetProvider(ctx).Bool(ViperKeyUseLegacyRequireVerifiedLoginError) } +func (p *Config) SelfServiceVerificationHookAutoInjectionDisabled(ctx context.Context) bool { + return p.GetProvider(ctx).Bool(ViperKeyDisableVerificationHookAutoInjection) +} + func (p *Config) SelfServiceFlowRecoveryEnabled(ctx context.Context) bool { return p.GetProvider(ctx).Bool(ViperKeySelfServiceRecoveryEnabled) } diff --git a/driver/config/config_test.go b/driver/config/config_test.go index ea108e8b6aad..82a95c227673 100644 --- a/driver/config/config_test.go +++ b/driver/config/config_test.go @@ -639,6 +639,17 @@ func TestSession(t *testing.T) { assert.Equal(t, true, p.SessionWhoAmICaching(ctx)) } +func TestVerificationHookAutoInjection(t *testing.T) { + t.Parallel() + ctx := context.Background() + l := logrusx.New("", "") + p := config.MustNew(t, l, &contextx.Default{}, configx.SkipValidation()) + + assert.Equal(t, false, p.SelfServiceVerificationHookAutoInjectionDisabled(ctx)) + p.MustSet(ctx, config.ViperKeyDisableVerificationHookAutoInjection, true) + assert.Equal(t, true, p.SelfServiceVerificationHookAutoInjectionDisabled(ctx)) +} + func TestCookies(t *testing.T) { t.Parallel() ctx := context.Background() diff --git a/driver/registry_default_registration.go b/driver/registry_default_registration.go index eccfa7c56d05..afcda186e1f5 100644 --- a/driver/registry_default_registration.go +++ b/driver/registry_default_registration.go @@ -35,8 +35,7 @@ func (m *RegistryDefault) PostRegistrationPostPersistHooks(ctx context.Context, } } - // WARNING - If you remove this, no verification emails / sms will be sent post-registration. - if m.Config().SelfServiceFlowVerificationEnabled(ctx) { + if m.Config().SelfServiceFlowVerificationEnabled(ctx) && !m.Config().SelfServiceVerificationHookAutoInjectionDisabled(ctx) { hooks = slices.Insert(hooks, 0, registration.PostHookPostPersistExecutor(m.HookVerifier())) } diff --git a/driver/registry_default_settings.go b/driver/registry_default_settings.go index 57d3d49e24ae..8d1ab560c0d7 100644 --- a/driver/registry_default_settings.go +++ b/driver/registry_default_settings.go @@ -33,7 +33,7 @@ func (m *RegistryDefault) PostSettingsPostPersistHooks(ctx context.Context, sett } } - if m.Config().SelfServiceFlowVerificationEnabled(ctx) { + if m.Config().SelfServiceFlowVerificationEnabled(ctx) && !m.Config().SelfServiceVerificationHookAutoInjectionDisabled(ctx) { hooks = slices.Insert(hooks, 0, settings.PostHookPostPersistExecutor(m.HookVerifier())) } diff --git a/driver/registry_default_test.go b/driver/registry_default_test.go index 1bdecd9dda3d..40bc1d902693 100644 --- a/driver/registry_default_test.go +++ b/driver/registry_default_test.go @@ -334,6 +334,38 @@ func TestDriverDefault_Hooks(t *testing.T) { } }, }, + { + uc: "Verification enabled but auto-injection disabled", + config: map[string]any{ + config.ViperKeySelfServiceVerificationEnabled: true, + config.ViperKeyDisableVerificationHookAutoInjection: true, + config.ViperKeySelfServiceRegistrationAfter + ".password.hooks": []map[string]any{ + {"hook": "session"}, + }, + }, + expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { + return []registration.PostHookPostPersistExecutor{ + hook.NewSessionIssuer(reg), + } + }, + }, + { + uc: "Auto-injection disabled but verification hook explicitly configured", + config: map[string]any{ + config.ViperKeySelfServiceVerificationEnabled: true, + config.ViperKeyDisableVerificationHookAutoInjection: true, + config.ViperKeySelfServiceRegistrationAfter + ".password.hooks": []map[string]any{ + {"hook": "verification"}, + {"hook": "session"}, + }, + }, + expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { + return []registration.PostHookPostPersistExecutor{ + hook.NewVerifier(reg), + hook.NewSessionIssuer(reg), + } + }, + }, } { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() @@ -608,6 +640,38 @@ func TestDriverDefault_Hooks(t *testing.T) { } }, }, + { + uc: "Verification enabled but auto-injection disabled", + config: map[string]any{ + config.ViperKeySelfServiceVerificationEnabled: true, + config.ViperKeyDisableVerificationHookAutoInjection: true, + config.ViperKeySelfServiceSettingsAfter + ".profile.hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, + }, + expect: func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor { + return []settings.PostHookPostPersistExecutor{ + hook.NewWebHook(reg, &request.Config{Method: "POST", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), + } + }, + }, + { + uc: "Auto-injection disabled but verification hook explicitly configured", + config: map[string]any{ + config.ViperKeySelfServiceVerificationEnabled: true, + config.ViperKeyDisableVerificationHookAutoInjection: true, + config.ViperKeySelfServiceSettingsAfter + ".profile.hooks": []map[string]any{ + {"hook": "verification"}, + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, + }, + expect: func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor { + return []settings.PostHookPostPersistExecutor{ + hook.NewVerifier(reg), + hook.NewWebHook(reg, &request.Config{Method: "POST", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), + } + }, + }, } { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 45734ffada7e..2e251583d3d7 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -800,6 +800,9 @@ { "$ref": "#/definitions/selfServiceShowVerificationUIHook" }, + { + "$ref": "#/definitions/selfServiceVerificationHook" + }, { "$ref": "#/definitions/b2bSSOHook" }, @@ -929,6 +932,9 @@ { "$ref": "#/definitions/selfServiceShowVerificationUIHook" }, + { + "$ref": "#/definitions/selfServiceVerificationHook" + }, { "$ref": "#/definitions/b2bSSOHook" } @@ -3247,6 +3253,12 @@ "description": "The node group to use for registration flows. Previously, the node group for the oidc method's profile fields was `oidc`. Going forward, it will be `default`. This switch can toggle between those two for backwards compatibility and will be removed in the future.", "default": false, "type": "boolean" + }, + "disable_verification_hook_auto_injection": { + "type": "boolean", + "title": "Disable automatic injection of the verification hook", + "description": "If true, the verification hook will not be automatically prepended to registration and settings post-persist hooks when verification is enabled. Operators can still explicitly add {\"hook\": \"verification\"} to their after-hooks configuration.", + "default": false } }, "additionalProperties": false diff --git a/selfservice/hook/verification_test.go b/selfservice/hook/verification_test.go index 4cfec6cf8e0b..513ff27d2467 100644 --- a/selfservice/hook/verification_test.go +++ b/selfservice/hook/verification_test.go @@ -152,6 +152,30 @@ func TestVerifier(t *testing.T) { }) } + t.Run("case=should not send verification emails when auto-injection is disabled", func(t *testing.T) { + t.Parallel() + conf, reg := pkg.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/verify.schema.json") + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.com/") + conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/") + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) + conf.MustSet(ctx, config.ViperKeyDisableVerificationHookAutoInjection, true) + + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.Traits = identity.Traits(`{"emails":["foo@ory.sh","bar@ory.sh"]}`) + require.NoError(t, reg.IdentityManager().Create(context.Background(), i)) + + hooks, err := reg.PostRegistrationPostPersistHooks(ctx, identity.CredentialsTypePassword) + require.NoError(t, err) + for _, h := range hooks { + assert.IsNotType(t, &hook.Verifier{}, h, "verifier hook should not be auto-injected when flag is set") + } + + messages, err := reg.CourierPersister().NextMessages(context.Background(), 12) + require.EqualError(t, err, courier.ErrQueueEmpty.Error()) + require.Len(t, messages, 0) + }) + t.Run("flow=login/case=does not run if aal is not 1", func(t *testing.T) { t.Parallel() _, reg := pkg.NewFastRegistryWithMocks(t)