// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package driver_test import ( "context" "crypto/sha256" "fmt" "net/http" "net/http/httptest" "testing" "github.com/gorilla/securecookie" "github.com/gorilla/sessions" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/ory/x/configx" "github.com/ory/x/contextx" "github.com/ory/x/logrusx" "github.com/ory/x/randx" "github.com/ory/kratos/continuity" "github.com/ory/kratos/driver" "github.com/ory/kratos/driver/config" "github.com/ory/kratos/hash" "github.com/ory/kratos/identity" "github.com/ory/kratos/pkg" "github.com/ory/kratos/request" "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/selfservice/flow/recovery" "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/selfservice/flow/settings" "github.com/ory/kratos/selfservice/flow/verification" "github.com/ory/kratos/selfservice/hook" ) func TestDriverDefault_Hooks(t *testing.T) { t.Parallel() ctx := context.Background() t.Run("type=verification", func(t *testing.T) { t.Parallel() // BEFORE hooks _, reg := pkg.NewVeryFastRegistryWithoutDB(t) for _, tc := range []struct { uc string config map[string]any expect func(reg *driver.RegistryDefault) []verification.PreHookExecutor }{ { uc: "No hooks configured", expect: func(reg *driver.RegistryDefault) []verification.PreHookExecutor { return []verification.PreHookExecutor{} }, }, { uc: "Two web_hooks are configured", config: map[string]any{ config.ViperKeySelfServiceVerificationBeforeHooks: []map[string]any{ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, }, }, expect: func(reg *driver.RegistryDefault) []verification.PreHookExecutor { return []verification.PreHookExecutor{ hook.NewWebHook(reg, &request.Config{Method: "POST", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), hook.NewWebHook(reg, &request.Config{Method: "GET", URL: "bar", Headers: map[string]string{"X-Custom-Header": "test"}}), } }, }, } { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() ctx := contextx.WithConfigValues(ctx, tc.config) h, err := reg.PreVerificationHooks(ctx) require.NoError(t, err) assert.Equal(t, tc.expect(reg), h) }) } // AFTER hooks for _, tc := range []struct { uc string prep func(conf *config.Config) config map[string]any expect func(reg *driver.RegistryDefault) []verification.PostHookExecutor }{ { uc: "No hooks configured", prep: func(conf *config.Config) {}, expect: func(reg *driver.RegistryDefault) []verification.PostHookExecutor { return []verification.PostHookExecutor{} }, }, { uc: "Multiple web_hooks configured", config: map[string]any{ config.ViperKeySelfServiceVerificationAfter + ".hooks": []map[string]any{ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, }, }, expect: func(reg *driver.RegistryDefault) []verification.PostHookExecutor { return []verification.PostHookExecutor{ hook.NewWebHook(reg, &request.Config{Method: "POST", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), hook.NewWebHook(reg, &request.Config{Method: "GET", URL: "bar", Headers: map[string]string{"X-Custom-Header": "test"}}), } }, }, } { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() ctx := contextx.WithConfigValues(ctx, tc.config) h, err := reg.PostVerificationHooks(ctx) require.NoError(t, err) assert.Equal(t, tc.expect(reg), h) }) } }) t.Run("type=recovery", func(t *testing.T) { t.Parallel() // BEFORE hooks for _, tc := range []struct { uc string config map[string]any expect func(reg *driver.RegistryDefault) []recovery.PreHookExecutor }{ { uc: "No hooks configured", expect: func(reg *driver.RegistryDefault) []recovery.PreHookExecutor { return []recovery.PreHookExecutor{} }, }, { uc: "Two web_hooks are configured", config: map[string]any{ config.ViperKeySelfServiceRecoveryBeforeHooks: []map[string]any{ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, }, }, expect: func(reg *driver.RegistryDefault) []recovery.PreHookExecutor { return []recovery.PreHookExecutor{ hook.NewWebHook(reg, &request.Config{Method: "POST", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), hook.NewWebHook(reg, &request.Config{Method: "GET", URL: "bar", Headers: map[string]string{"X-Custom-Header": "test"}}), } }, }, } { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() ctx := contextx.WithConfigValues(ctx, tc.config) _, reg := pkg.NewVeryFastRegistryWithoutDB(t) h, err := reg.PreRecoveryHooks(ctx) require.NoError(t, err) assert.Equal(t, tc.expect(reg), h) }) } // AFTER hooks for _, tc := range []struct { uc string config map[string]any expect func(reg *driver.RegistryDefault) []recovery.PostHookExecutor }{ { uc: "No hooks configured", expect: func(reg *driver.RegistryDefault) []recovery.PostHookExecutor { return []recovery.PostHookExecutor{} }, }, { uc: "Multiple web_hooks configured", config: map[string]any{ config.ViperKeySelfServiceRecoveryAfter + ".hooks": []map[string]any{ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, }, }, expect: func(reg *driver.RegistryDefault) []recovery.PostHookExecutor { return []recovery.PostHookExecutor{ hook.NewWebHook(reg, &request.Config{Method: "POST", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), hook.NewWebHook(reg, &request.Config{Method: "GET", URL: "bar", Headers: map[string]string{"X-Custom-Header": "test"}}), } }, }, } { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() ctx := contextx.WithConfigValues(ctx, tc.config) _, reg := pkg.NewVeryFastRegistryWithoutDB(t) h, err := reg.PostRecoveryHooks(ctx) require.NoError(t, err) assert.Equal(t, tc.expect(reg), h) }) } }) t.Run("type=registration", func(t *testing.T) { t.Parallel() // BEFORE hooks for _, tc := range []struct { uc string config map[string]any expect func(reg *driver.RegistryDefault) []registration.PreHookExecutor }{ { uc: "No hooks configured", expect: func(reg *driver.RegistryDefault) []registration.PreHookExecutor { return []registration.PreHookExecutor{} }, }, { uc: "Two web_hooks are configured", config: map[string]any{ config.ViperKeySelfServiceRegistrationBeforeHooks: []map[string]any{ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, }, }, expect: func(reg *driver.RegistryDefault) []registration.PreHookExecutor { return []registration.PreHookExecutor{ hook.NewWebHook(reg, &request.Config{Method: "POST", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), hook.NewWebHook(reg, &request.Config{Method: "GET", URL: "bar", Headers: map[string]string{"X-Custom-Header": "test"}}), } }, }, } { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() ctx := contextx.WithConfigValues(ctx, tc.config) _, reg := pkg.NewVeryFastRegistryWithoutDB(t) h, err := reg.PreRegistrationHooks(ctx) require.NoError(t, err) assert.EqualValues(t, tc.expect(reg), h) }) } // AFTER hooks for _, tc := range []struct { uc string config map[string]any expect func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor }{ { uc: "No hooks configured", expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return []registration.PostHookPostPersistExecutor{} }, }, { uc: "Only session hook configured for password strategy", config: map[string]any{ config.ViperKeySelfServiceVerificationEnabled: true, config.ViperKeySelfServiceRegistrationAfter + ".password.hooks": []map[string]any{ {"hook": "session"}, }, }, expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return []registration.PostHookPostPersistExecutor{ hook.NewVerifier(reg), hook.NewSessionIssuer(reg), } }, }, { uc: "A session hook and a web_hook are configured for password strategy", config: map[string]any{ config.ViperKeySelfServiceVerificationEnabled: true, config.ViperKeySelfServiceRegistrationAfter + ".password.hooks": []map[string]any{ {"hook": "web_hook", "config": map[string]any{"headers": map[string]string{"X-Custom-Header": "test"}, "url": "foo", "method": "POST", "body": "bar"}}, {"hook": "session"}, }, }, expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return []registration.PostHookPostPersistExecutor{ hook.NewVerifier(reg), hook.NewWebHook(reg, &request.Config{URL: "foo", Method: "POST", TemplateURI: "bar", Headers: map[string]string{"X-Custom-Header": "test"}}), hook.NewSessionIssuer(reg), } }, }, { uc: "Two web_hooks are configured on a global level", config: map[string]any{ config.ViperKeySelfServiceRegistrationAfter + ".hooks": []map[string]any{ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, }, }, expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return []registration.PostHookPostPersistExecutor{ hook.NewWebHook(reg, &request.Config{Method: "POST", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), hook.NewWebHook(reg, &request.Config{Method: "GET", URL: "bar", Headers: map[string]string{"X-Custom-Header": "test"}}), } }, }, { uc: "Hooks are configured on a global level, as well as on a strategy level", config: map[string]any{ config.ViperKeySelfServiceRegistrationAfter + ".password.hooks": []map[string]any{ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, {"hook": "session"}, }, config.ViperKeySelfServiceRegistrationAfter + ".hooks": []map[string]any{ {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, }, config.ViperKeySelfServiceVerificationEnabled: true, }, expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return []registration.PostHookPostPersistExecutor{ hook.NewVerifier(reg), hook.NewWebHook(reg, &request.Config{Method: "GET", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), hook.NewSessionIssuer(reg), } }, }, { uc: "show_verification_ui is configured", config: map[string]any{ config.ViperKeySelfServiceRegistrationAfter + ".hooks": []map[string]any{ {"hook": "show_verification_ui"}, }, }, expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return []registration.PostHookPostPersistExecutor{ hook.NewShowVerificationUIHook(reg), } }, }, } { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() ctx := contextx.WithConfigValues(ctx, tc.config) _, reg := pkg.NewVeryFastRegistryWithoutDB(t) h, err := reg.PostRegistrationPostPersistHooks(ctx, identity.CredentialsTypePassword) require.NoError(t, err) assert.Equal(t, tc.expect(reg), h) }) } }) t.Run("type=login", func(t *testing.T) { t.Parallel() // BEFORE hooks for _, tc := range []struct { uc string config map[string]any expect func(reg *driver.RegistryDefault) []login.PreHookExecutor }{ { uc: "No hooks configured", expect: func(reg *driver.RegistryDefault) []login.PreHookExecutor { return []login.PreHookExecutor{} }, }, { uc: "Two web_hooks are configured", config: map[string]any{ config.ViperKeySelfServiceLoginBeforeHooks: []map[string]any{ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, }, }, expect: func(reg *driver.RegistryDefault) []login.PreHookExecutor { return []login.PreHookExecutor{ hook.NewWebHook(reg, &request.Config{Method: "POST", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), hook.NewWebHook(reg, &request.Config{Method: "GET", URL: "bar", Headers: map[string]string{"X-Custom-Header": "test"}}), } }, }, } { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() ctx := contextx.WithConfigValues(ctx, tc.config) _, reg := pkg.NewVeryFastRegistryWithoutDB(t) h, err := reg.PreLoginHooks(ctx) require.NoError(t, err) assert.Equal(t, tc.expect(reg), h) }) } // AFTER hooks for _, tc := range []struct { uc string config map[string]any expect func(reg *driver.RegistryDefault) []login.PostHookExecutor }{ { uc: "No hooks configured", expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return []login.PostHookExecutor{} }, }, { uc: "Only revoke_active_sessions hook configured for password strategy", config: map[string]any{ config.ViperKeySelfServiceLoginAfter + ".password.hooks": []map[string]any{ {"hook": "revoke_active_sessions"}, }, }, expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return []login.PostHookExecutor{ hook.NewSessionDestroyer(reg), } }, }, { uc: "Only require_verified_address hook configured for password strategy", config: map[string]any{ config.ViperKeySelfServiceLoginAfter + ".password.hooks": []map[string]any{ {"hook": "require_verified_address"}, }, }, expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return []login.PostHookExecutor{ hook.NewAddressVerifier(reg), } }, }, { uc: "A revoke_active_sessions hook, require_verified_address hook and a web_hook are configured for password strategy", config: map[string]any{ config.ViperKeySelfServiceLoginAfter + ".password.hooks": []map[string]any{ {"hook": "web_hook", "config": map[string]any{"headers": map[string]string{"X-Custom-Header": "test"}, "url": "foo", "method": "POST", "body": "bar"}}, {"hook": "require_verified_address"}, {"hook": "revoke_active_sessions"}, }, }, expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return []login.PostHookExecutor{ hook.NewWebHook(reg, &request.Config{TemplateURI: "bar", Method: "POST", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), hook.NewAddressVerifier(reg), hook.NewSessionDestroyer(reg), } }, }, { uc: "Two web_hooks are configured on a global level", config: map[string]any{ config.ViperKeySelfServiceLoginAfter + ".hooks": []map[string]any{ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, }, }, expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return []login.PostHookExecutor{ hook.NewWebHook(reg, &request.Config{Method: "POST", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), hook.NewWebHook(reg, &request.Config{Method: "GET", URL: "bar", Headers: map[string]string{"X-Custom-Header": "test"}}), } }, }, { uc: "Hooks are configured on a global level, as well as on a strategy level", config: map[string]any{ config.ViperKeySelfServiceLoginAfter + ".password.hooks": []map[string]any{ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, {"hook": "revoke_active_sessions"}, {"hook": "require_verified_address"}, }, config.ViperKeySelfServiceLoginAfter + ".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) []login.PostHookExecutor { return []login.PostHookExecutor{ hook.NewWebHook(reg, &request.Config{Method: "GET", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), hook.NewSessionDestroyer(reg), hook.NewAddressVerifier(reg), } }, }, } { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() ctx := contextx.WithConfigValues(ctx, tc.config) _, reg := pkg.NewVeryFastRegistryWithoutDB(t) h, err := reg.PostLoginHooks(ctx, identity.CredentialsTypePassword) require.NoError(t, err) assert.Equal(t, tc.expect(reg), h) }) } }) t.Run("type=settings", func(t *testing.T) { t.Parallel() // BEFORE hooks for _, tc := range []struct { uc string config map[string]any expect func(reg *driver.RegistryDefault) []settings.PreHookExecutor }{ { uc: "No hooks configured", expect: func(reg *driver.RegistryDefault) []settings.PreHookExecutor { return []settings.PreHookExecutor{} }, }, { uc: "Two web_hooks are configured", config: map[string]any{ config.ViperKeySelfServiceSettingsBeforeHooks: []map[string]any{ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, }, }, expect: func(reg *driver.RegistryDefault) []settings.PreHookExecutor { return []settings.PreHookExecutor{ hook.NewWebHook(reg, &request.Config{Method: "POST", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), hook.NewWebHook(reg, &request.Config{Method: "GET", URL: "bar", Headers: map[string]string{"X-Custom-Header": "test"}}), } }, }, } { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { t.Parallel() ctx := contextx.WithConfigValues(ctx, tc.config) _, reg := pkg.NewVeryFastRegistryWithoutDB(t) h, err := reg.PreSettingsHooks(ctx) require.NoError(t, err) assert.Equal(t, tc.expect(reg), h) }) } // AFTER hooks for _, tc := range []struct { uc string config map[string]any expect func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor }{ { uc: "No hooks configured", expect: func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor { return []settings.PostHookPostPersistExecutor{} }, }, { uc: "Only verify hook configured for the strategy", config: map[string]any{ config.ViperKeySelfServiceVerificationEnabled: true, // I think this is a bug as there is a hook named verify defined for both profile and password // strategies. Instead of using it, the code makes use of the property used above and which // is defined in an entirely different flow (verification). }, expect: func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor { return []settings.PostHookPostPersistExecutor{ hook.NewVerifier(reg), } }, }, { uc: "A verify hook and a web_hook are configured for profile strategy", config: map[string]any{ config.ViperKeySelfServiceSettingsAfter + ".profile.hooks": []map[string]any{ {"hook": "web_hook", "config": map[string]any{"headers": map[string]string{"X-Custom-Header": "test"}, "url": "foo", "method": "POST", "body": "bar"}}, }, config.ViperKeySelfServiceVerificationEnabled: true, }, expect: func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor { return []settings.PostHookPostPersistExecutor{ hook.NewVerifier(reg), hook.NewWebHook(reg, &request.Config{TemplateURI: "bar", Method: "POST", URL: "foo", Headers: map[string]string{"X-Custom-Header": "test"}}), } }, }, { uc: "Two web_hooks are configured on a global level", config: map[string]any{ config.ViperKeySelfServiceSettingsAfter + ".hooks": []map[string]any{ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "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"}}), hook.NewWebHook(reg, &request.Config{Method: "GET", URL: "bar", Headers: map[string]string{"X-Custom-Header": "test"}}), } }, }, { uc: "Hooks are configured on a global level, as well as on a strategy level", config: map[string]any{ config.ViperKeySelfServiceVerificationEnabled: true, config.ViperKeySelfServiceSettingsAfter + ".profile.hooks": []map[string]any{ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, }, config.ViperKeySelfServiceSettingsAfter + ".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.NewVerifier(reg), hook.NewWebHook(reg, &request.Config{Method: "GET", 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() ctx := contextx.WithConfigValues(ctx, tc.config) _, reg := pkg.NewVeryFastRegistryWithoutDB(t) h, err := reg.PostSettingsPostPersistHooks(ctx, "profile") require.NoError(t, err) assert.Equal(t, tc.expect(reg), h) }) } }) } func TestDriverDefault_Strategies(t *testing.T) { t.Parallel() ctx := context.Background() t.Run("case=registration", func(t *testing.T) { t.Parallel() for _, tc := range []struct { name string config map[string]any expect []string }{ { name: "no strategies", config: map[string]any{ config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, expect: []string{"profile"}, }, { name: "only password", config: map[string]any{ config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, expect: []string{"profile", "password"}, }, { name: "oidc and password", config: map[string]any{ config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": true, config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, expect: []string{"profile", "password", "oidc"}, }, { name: "oidc, password and totp", config: map[string]any{ config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": true, config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, config.ViperKeySelfServiceStrategyConfig + ".totp.enabled": true, config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, expect: []string{"profile", "password", "oidc"}, }, { name: "password and code", config: map[string]any{ config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, config.ViperKeySelfServiceStrategyConfig + ".code.enabled": true, }, expect: []string{"profile", "password", "code"}, }, } { t.Run(fmt.Sprintf("subcase=%s", tc.name), func(t *testing.T) { t.Parallel() ctx := contextx.WithConfigValues(ctx, tc.config) _, reg := pkg.NewVeryFastRegistryWithoutDB(t) s := reg.RegistrationStrategies(ctx) require.Len(t, s, len(tc.expect)) for k, e := range tc.expect { assert.Equal(t, e, s[k].ID().String()) } }) } }) t.Run("case=login", func(t *testing.T) { t.Parallel() for _, tc := range []struct { name string config map[string]any expect []string }{ { name: "no strategies", config: map[string]any{ config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, }, { name: "only password", config: map[string]any{ config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, expect: []string{"password"}, }, { name: "oidc and password", config: map[string]any{ config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": true, config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, expect: []string{"password", "oidc"}, }, { name: "oidc, password and totp", config: map[string]any{ config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": true, config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, config.ViperKeySelfServiceStrategyConfig + ".totp.enabled": true, config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, expect: []string{"password", "oidc", "totp"}, }, { name: "password and code", config: map[string]any{ config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, config.ViperKeySelfServiceStrategyConfig + ".code.enabled": true, }, expect: []string{"password", "code"}, }, { name: "code is enabled if passwordless_enabled is true", config: map[string]any{ config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, config.ViperKeySelfServiceStrategyConfig + ".code.passwordless_enabled": true, }, expect: []string{"code"}, }, } { t.Run(fmt.Sprintf("run=%s", tc.name), func(t *testing.T) { t.Parallel() ctx := contextx.WithConfigValues(ctx, tc.config) _, reg := pkg.NewVeryFastRegistryWithoutDB(t) s := reg.LoginStrategies(ctx) require.Len(t, s, len(tc.expect)) for k, e := range tc.expect { assert.Equal(t, e, s[k].ID().String()) } }) } }) t.Run("case=recovery", func(t *testing.T) { t.Parallel() for k, tc := range []struct { config map[string]any expect []string }{ { config: map[string]any{ config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, config.ViperKeySelfServiceStrategyConfig + ".link.enabled": false, }, }, { config: map[string]any{ config.ViperKeySelfServiceStrategyConfig + ".code.enabled": true, config.ViperKeySelfServiceStrategyConfig + ".link.enabled": true, }, expect: []string{"code", "link"}, }, } { t.Run(fmt.Sprintf("run=%d", k), func(t *testing.T) { t.Parallel() ctx := contextx.WithConfigValues(ctx, tc.config) _, reg := pkg.NewVeryFastRegistryWithoutDB(t) s := reg.RecoveryStrategies(ctx) require.Len(t, s, len(tc.expect)) for k, e := range tc.expect { assert.Equal(t, e, s[k].RecoveryStrategyID()) } }) } }) t.Run("case=settings", func(t *testing.T) { t.Parallel() l := logrusx.New("", "") for k, tc := range []struct { configOptions []configx.OptionModifier expect []string }{ { configOptions: []configx.OptionModifier{configx.WithValues(map[string]any{ config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": false, config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": false, })}, }, { configOptions: []configx.OptionModifier{configx.WithValues(map[string]any{ config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": true, config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, })}, expect: []string{"profile"}, }, { configOptions: []configx.OptionModifier{configx.WithValues(map[string]any{ config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": true, config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, config.ViperKeySelfServiceStrategyConfig + ".totp.enabled": true, })}, expect: []string{"profile", "totp"}, }, { configOptions: []configx.OptionModifier{configx.WithValues(map[string]any{ config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, })}, expect: []string{"profile", "password"}, }, { configOptions: []configx.OptionModifier{ configx.WithConfigFiles("../test/e2e/profiles/verification/.kratos.yml"), configx.WithValue(config.ViperKeyDSN, config.DefaultSQLiteMemoryDSN), }, expect: []string{"profile", "password"}, }, } { t.Run(fmt.Sprintf("run=%d", k), func(t *testing.T) { conf := config.MustNew(t, l, &contextx.Default{}, append(tc.configOptions, configx.SkipValidation())...) reg, err := driver.NewRegistryFromDSN(ctx, conf, l) require.NoError(t, err) s := reg.SettingsStrategies(ctx) require.Len(t, s, len(tc.expect)) for k, e := range tc.expect { assert.Equal(t, e, s[k].SettingsStrategyID()) } }) } }) } func TestDefaultRegistry_AllStrategies(t *testing.T) { t.Parallel() _, reg := pkg.NewVeryFastRegistryWithoutDB(t) t.Run("case=all login strategies", func(t *testing.T) { expects := []string{"password", "oidc", "code", "totp", "passkey", "webauthn", "lookup_secret", "identifier_first"} s := reg.AllLoginStrategies() require.Len(t, s, len(expects)) for k, e := range expects { assert.Equal(t, e, s[k].ID().String()) } }) t.Run("case=all registration strategies", func(t *testing.T) { expects := []string{"profile", "password", "oidc", "code", "passkey", "webauthn"} s := reg.AllRegistrationStrategies() require.Len(t, s, len(expects)) for k, e := range expects { assert.Equal(t, e, s[k].ID().String()) } }) t.Run("case=all settings strategies", func(t *testing.T) { expects := []string{"profile", "password", "oidc", "totp", "passkey", "webauthn", "lookup_secret"} s := reg.AllSettingsStrategies() require.Len(t, s, len(expects)) for k, e := range expects { assert.Equal(t, e, s[k].SettingsStrategyID()) } }) t.Run("case=all recovery strategies", func(t *testing.T) { expects := []string{"code", "link"} s := reg.AllRecoveryStrategies() require.Len(t, s, len(expects)) for k, e := range expects { assert.Equal(t, e, s[k].RecoveryStrategyID()) } }) } func TestGetActiveRecoveryStrategy(t *testing.T) { t.Parallel() ctx := context.Background() _, reg := pkg.NewVeryFastRegistryWithoutDB(t) t.Run("returns error if active strategy is disabled", func(t *testing.T) { ctx := contextx.WithConfigValues(ctx, map[string]any{ "selfservice.methods.code.enabled": false, config.ViperKeySelfServiceRecoveryUse: "code", }) _, _, err := reg.GetActiveRecoveryStrategies(ctx) require.Error(t, err) }) t.Run("returns active strategies", func(t *testing.T) { for _, sID := range []string{ "code", "link", } { t.Run(fmt.Sprintf("strategy=%s", sID), func(t *testing.T) { ctx := contextx.WithConfigValues(ctx, map[string]any{ fmt.Sprintf("selfservice.methods.%s.enabled", sID): true, config.ViperKeySelfServiceRecoveryUse: sID, }) s, ps, err := reg.GetActiveRecoveryStrategies(ctx) require.NoError(t, err) require.Len(t, s, 1) require.Equal(t, sID, ps.RecoveryStrategyID()) }) } }) } func TestGetActiveVerificationStrategy(t *testing.T) { t.Parallel() ctx := context.Background() _, reg := pkg.NewVeryFastRegistryWithoutDB(t) t.Run("returns error if active strategy is disabled", func(t *testing.T) { ctx := contextx.WithConfigValues(ctx, map[string]any{ "selfservice.methods.code.enabled": false, config.ViperKeySelfServiceVerificationUse: "code", }) _, _, err := reg.GetActiveVerificationStrategies(ctx) require.Error(t, err) }) t.Run("returns active strategy", func(t *testing.T) { for _, sID := range []string{ "code", "link", } { t.Run(fmt.Sprintf("strategy=%s", sID), func(t *testing.T) { ctx := contextx.WithConfigValues(ctx, map[string]any{ fmt.Sprintf("selfservice.methods.%s.enabled", sID): true, config.ViperKeySelfServiceVerificationUse: sID, }) _, s, err := reg.GetActiveVerificationStrategies(ctx) require.NoError(t, err) require.Equal(t, sID, s.VerificationStrategyID()) }) } }) } func TestDriverDefault_ExtraHashers(t *testing.T) { t.Parallel() // The OSS config schema only allows argon2 and bcrypt. The context // override simulates a downstream distribution whose patched schema // permits the extra algorithm. ctx := contextx.WithConfigValue(t.Context(), config.ViperKeyHasherAlgorithm, "pbkdf2") _, reg := pkg.NewFastRegistryWithMocks(t) reg.WithHashers(map[string]driver.NewHasherFn{ "pbkdf2": func(_ driver.Registry) hash.Hasher { return &hash.Pbkdf2{Algorithm: "sha256", Iterations: 100_000, SaltLength: 16, KeyLength: 32} }, }) h := reg.Hasher(ctx) require.IsType(t, &hash.Pbkdf2{}, h) generated, err := h.Generate(ctx, []byte("password")) require.NoError(t, err) assert.True(t, hash.IsPbkdf2Sha256Hash(generated), "%s", generated) require.NoError(t, hash.Compare(ctx, []byte("password"), generated)) } func TestContinuityCookieManager(t *testing.T) { t.Parallel() const sensitiveValue = "continuity-container-reference" writeCookie := func(t *testing.T, store sessions.Store) *http.Cookie { t.Helper() r := httptest.NewRequest(http.MethodGet, "https://www.ory.sh/", nil) w := httptest.NewRecorder() sess, err := store.New(r, continuity.CookieName) require.NoError(t, err) sess.Values["value"] = sensitiveValue require.NoError(t, store.Save(r, w, sess)) cookies := w.Result().Cookies() require.Len(t, cookies, 1) return cookies[0] } t.Run("case=encrypts the continuity cookie", func(t *testing.T) { t.Parallel() _, reg := pkg.NewVeryFastRegistryWithoutDB(t) secret := randx.MustString(32, randx.AlphaNum) ctx := contextx.WithConfigValue(t.Context(), config.ViperKeySecretsCookie, []string{secret}) cookie := writeCookie(t, reg.ContinuityCookieManager(ctx)) // A codec that only knows the signing key must not be able to decode the // cookie. If it can, the cookie is signed but not encrypted. values := map[any]any{} signatureOnly := securecookie.New([]byte(secret), nil) require.Errorf(t, signatureOnly.Decode(continuity.CookieName, cookie.Value, &values), "the continuity cookie must be encrypted, but decoding it without the encryption key succeeded: %+v", values) // The cookie must decode with the same key pairing CookieManager uses: // hash key = secret, block key = SHA-256(secret). blockKey := sha256.Sum256([]byte(secret)) withEncryption := securecookie.New([]byte(secret), blockKey[:]) require.NoError(t, withEncryption.Decode(continuity.CookieName, cookie.Value, &values)) assert.Equal(t, sensitiveValue, values["value"]) }) t.Run("case=decodes cookies issued before secret rotation", func(t *testing.T) { t.Parallel() _, reg := pkg.NewVeryFastRegistryWithoutDB(t) oldSecret := randx.MustString(32, randx.AlphaNum) newSecret := randx.MustString(32, randx.AlphaNum) oldCtx := contextx.WithConfigValue(t.Context(), config.ViperKeySecretsCookie, []string{oldSecret}) cookie := writeCookie(t, reg.ContinuityCookieManager(oldCtx)) rotatedCtx := contextx.WithConfigValue(t.Context(), config.ViperKeySecretsCookie, []string{newSecret, oldSecret}) r := httptest.NewRequest(http.MethodGet, "https://www.ory.sh/", nil) r.AddCookie(cookie) sess, err := reg.ContinuityCookieManager(rotatedCtx).New(r, continuity.CookieName) require.NoError(t, err) assert.Equal(t, sensitiveValue, sess.Values["value"]) }) // Cookies issued before the key pairing fix used the raw secrets as // (hashKey, blockKey) pairs. They must remain readable during rollout. t.Run("case=decodes legacy cookie signed with a single secret", func(t *testing.T) { t.Parallel() _, reg := pkg.NewVeryFastRegistryWithoutDB(t) secret := randx.MustString(32, randx.AlphaNum) ctx := contextx.WithConfigValue(t.Context(), config.ViperKeySecretsCookie, []string{secret}) // The legacy store had a single signed-but-unencrypted codec. legacy := securecookie.New([]byte(secret), nil) encoded, err := legacy.Encode(continuity.CookieName, map[any]any{"value": sensitiveValue}) require.NoError(t, err) r := httptest.NewRequest(http.MethodGet, "https://www.ory.sh/", nil) r.AddCookie(&http.Cookie{Name: continuity.CookieName, Value: encoded}) sess, err := reg.ContinuityCookieManager(ctx).New(r, continuity.CookieName) require.NoError(t, err) assert.Equal(t, sensitiveValue, sess.Values["value"]) }) t.Run("case=decodes legacy cookie encrypted with the second secret", func(t *testing.T) { t.Parallel() _, reg := pkg.NewVeryFastRegistryWithoutDB(t) firstSecret := randx.MustString(32, randx.AlphaNum) secondSecret := randx.MustString(32, randx.AlphaNum) ctx := contextx.WithConfigValue(t.Context(), config.ViperKeySecretsCookie, []string{firstSecret, secondSecret}) // The legacy store treated the second secret as the AES block key of // the first codec. legacy := securecookie.New([]byte(firstSecret), []byte(secondSecret)) encoded, err := legacy.Encode(continuity.CookieName, map[any]any{"value": sensitiveValue}) require.NoError(t, err) r := httptest.NewRequest(http.MethodGet, "https://www.ory.sh/", nil) r.AddCookie(&http.Cookie{Name: continuity.CookieName, Value: encoded}) sess, err := reg.ContinuityCookieManager(ctx).New(r, continuity.CookieName) require.NoError(t, err) assert.Equal(t, sensitiveValue, sess.Values["value"]) }) }