Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
1102 lines
40 KiB
Go
1102 lines
40 KiB
Go
// 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"])
|
|
})
|
|
}
|