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
1597 lines
63 KiB
Go
1597 lines
63 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package config_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/ory/x/contextx"
|
|
|
|
"github.com/ory/x/httpx"
|
|
"github.com/ory/x/randx"
|
|
|
|
"github.com/ory/x/snapshotx"
|
|
|
|
"github.com/ghodss/yaml"
|
|
|
|
"github.com/go-webauthn/webauthn/protocol"
|
|
|
|
"github.com/ory/kratos/pkg/testhelpers"
|
|
|
|
"github.com/ory/x/configx"
|
|
|
|
"github.com/sirupsen/logrus/hooks/test"
|
|
|
|
"github.com/ory/x/logrusx"
|
|
"github.com/ory/x/urlx"
|
|
|
|
_ "github.com/ory/jsonschema/v3/fileloader"
|
|
|
|
"github.com/gofrs/uuid"
|
|
|
|
"github.com/ory/kratos/driver/config"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestViperProvider(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
t.Cleanup(cancel)
|
|
|
|
t.Run("suite=loaders", func(t *testing.T) {
|
|
p := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.yaml"), configx.WithContext(ctx))
|
|
|
|
t.Run("group=client config", func(t *testing.T) {
|
|
assert.False(t, p.ClientHTTPNoPrivateIPRanges(ctx), "Should not have private IP ranges disabled per default")
|
|
assert.Equal(t, []string{}, p.ClientHTTPPrivateIPExceptionURLs(ctx), "Should return the correct exceptions")
|
|
|
|
p.MustSet(ctx, config.ViperKeyClientHTTPNoPrivateIPRanges, true)
|
|
assert.True(t, p.ClientHTTPNoPrivateIPRanges(ctx), "Should disallow private IP ranges if set")
|
|
|
|
p.MustSet(ctx, config.ViperKeyClientHTTPPrivateIPExceptionURLs, []string{"https://foobar.com/baz"})
|
|
assert.Equal(t, []string{"https://foobar.com/baz"}, p.ClientHTTPPrivateIPExceptionURLs(ctx), "Should return the correct exceptions")
|
|
})
|
|
|
|
t.Run("group=urls", func(t *testing.T) {
|
|
assert.Equal(t, "http://test.kratos.ory.sh/login", p.SelfServiceFlowLoginUI(ctx).String())
|
|
assert.Equal(t, "http://test.kratos.ory.sh/settings", p.SelfServiceFlowSettingsUI(ctx).String())
|
|
assert.Equal(t, "http://test.kratos.ory.sh/register", p.SelfServiceFlowRegistrationUI(ctx).String())
|
|
assert.Equal(t, "http://test.kratos.ory.sh/error", p.SelfServiceFlowErrorURL(ctx).String())
|
|
|
|
assert.Equal(t, "http://admin.kratos.ory.sh", p.SelfAdminURL(ctx).String())
|
|
assert.Equal(t, "http://public.kratos.ory.sh", p.SelfPublicURL(ctx).String())
|
|
|
|
var ds []string
|
|
for _, v := range p.SelfServiceBrowserAllowedReturnToDomains(ctx) {
|
|
ds = append(ds, v.String())
|
|
}
|
|
|
|
assert.Equal(t, []string{
|
|
"http://return-to-1-test.ory.sh/",
|
|
"http://return-to-2-test.ory.sh/",
|
|
"http://*.wildcards.ory.sh",
|
|
"/return-to-relative-test/",
|
|
}, ds)
|
|
|
|
pWithFragments := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.WithValues(map[string]interface{}{
|
|
config.ViperKeySelfServiceLoginUI: "http://test.kratos.ory.sh/#/login",
|
|
config.ViperKeySelfServiceSettingsURL: "http://test.kratos.ory.sh/#/settings",
|
|
config.ViperKeySelfServiceRegistrationUI: "http://test.kratos.ory.sh/#/register",
|
|
config.ViperKeySelfServiceErrorUI: "http://test.kratos.ory.sh/#/error",
|
|
}), configx.SkipValidation())
|
|
|
|
assert.Equal(t, "http://test.kratos.ory.sh/#/login", pWithFragments.SelfServiceFlowLoginUI(ctx).String())
|
|
assert.Equal(t, "http://test.kratos.ory.sh/#/settings", pWithFragments.SelfServiceFlowSettingsUI(ctx).String())
|
|
assert.Equal(t, "http://test.kratos.ory.sh/#/register", pWithFragments.SelfServiceFlowRegistrationUI(ctx).String())
|
|
assert.Equal(t, "http://test.kratos.ory.sh/#/error", pWithFragments.SelfServiceFlowErrorURL(ctx).String())
|
|
|
|
pWithRelativeFragments := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.WithValues(map[string]interface{}{
|
|
config.ViperKeySelfServiceLoginUI: "/login",
|
|
config.ViperKeySelfServiceSettingsURL: "/settings",
|
|
config.ViperKeySelfServiceRegistrationUI: "/register",
|
|
config.ViperKeySelfServiceErrorUI: "/error",
|
|
}), configx.SkipValidation())
|
|
|
|
assert.Equal(t, "/login", pWithRelativeFragments.SelfServiceFlowLoginUI(ctx).String())
|
|
assert.Equal(t, "/settings", pWithRelativeFragments.SelfServiceFlowSettingsUI(ctx).String())
|
|
assert.Equal(t, "/register", pWithRelativeFragments.SelfServiceFlowRegistrationUI(ctx).String())
|
|
assert.Equal(t, "/error", pWithRelativeFragments.SelfServiceFlowErrorURL(ctx).String())
|
|
|
|
for _, v := range []string{
|
|
"#/login",
|
|
"test.kratos.ory.sh/login",
|
|
} {
|
|
logger := logrusx.New("", "")
|
|
logger.Logger.ExitFunc = func(code int) { panic("") }
|
|
hook := new(test.Hook)
|
|
logger.Logger.Hooks.Add(hook)
|
|
|
|
pWithIncorrectUrls := config.MustNew(t, logger, &contextx.Default{}, configx.WithValues(map[string]interface{}{
|
|
config.ViperKeySelfServiceLoginUI: v,
|
|
}), configx.SkipValidation())
|
|
|
|
assert.Panics(t, func() { pWithIncorrectUrls.SelfServiceFlowLoginUI(ctx) })
|
|
|
|
assert.Equal(t, logrus.FatalLevel, hook.LastEntry().Level)
|
|
assert.Equal(t, "Configuration value from key selfservice.flows.login.ui_url is not a valid URL: "+v, hook.LastEntry().Message)
|
|
assert.Equal(t, 1, len(hook.Entries))
|
|
}
|
|
})
|
|
|
|
t.Run("group=default_return_to", func(t *testing.T) {
|
|
assert.Equal(t, "https://self-service/login/password/return_to", p.SelfServiceFlowLoginReturnTo(ctx, "password").String())
|
|
assert.Equal(t, "https://self-service/login/return_to", p.SelfServiceFlowLoginReturnTo(ctx, "oidc").String())
|
|
|
|
assert.Equal(t, "https://self-service/registration/return_to", p.SelfServiceFlowRegistrationReturnTo(ctx, "password").String())
|
|
assert.Equal(t, "https://self-service/registration/oidc/return_to", p.SelfServiceFlowRegistrationReturnTo(ctx, "oidc").String())
|
|
|
|
assert.Equal(t, "https://self-service/settings/password/return_to", p.SelfServiceFlowSettingsReturnTo(ctx, "password", p.SelfServiceBrowserDefaultReturnTo(ctx)).String())
|
|
assert.Equal(t, "https://self-service/settings/return_to", p.SelfServiceFlowSettingsReturnTo(ctx, "profile", p.SelfServiceBrowserDefaultReturnTo(ctx)).String())
|
|
|
|
assert.Equal(t, "http://test.kratos.ory.sh:4000/", p.SelfServiceFlowLogoutRedirectURL(ctx).String())
|
|
p.MustSet(ctx, config.ViperKeySelfServiceLogoutBrowserDefaultReturnTo, "")
|
|
assert.Equal(t, "http://return-to-3-test.ory.sh/", p.SelfServiceFlowLogoutRedirectURL(ctx).String())
|
|
})
|
|
|
|
t.Run("group=identity", func(t *testing.T) {
|
|
c := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.mock.identities.yaml"), configx.SkipValidation())
|
|
|
|
ds, err := c.DefaultIdentityTraitsSchemaURL(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "http://test.kratos.ory.sh/default-identity.schema.json", ds.String())
|
|
|
|
ss, err := c.IdentityTraitsSchemas(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 2, len(ss))
|
|
assert.Contains(t, ss, config.Schema{
|
|
ID: "default",
|
|
URL: "http://test.kratos.ory.sh/default-identity.schema.json",
|
|
})
|
|
assert.Contains(t, ss, config.Schema{
|
|
ID: "other",
|
|
URL: "http://test.kratos.ory.sh/other-identity.schema.json",
|
|
})
|
|
|
|
ds, err = c.IdentityTraitsSchemaURL(ctx, "other")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "http://test.kratos.ory.sh/other-identity.schema.json", ds.String())
|
|
|
|
ds, err = c.IdentityTraitsSchemaURL(ctx, "default")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "http://test.kratos.ory.sh/default-identity.schema.json", ds.String())
|
|
|
|
ds, err = c.IdentityTraitsSchemaURL(ctx, "")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "http://test.kratos.ory.sh/default-identity.schema.json", ds.String())
|
|
|
|
ds, err = c.IdentityTraitsSchemaURL(ctx, "does-not-exist")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "http://test.kratos.ory.sh/default-identity.schema.json", ds.String())
|
|
})
|
|
|
|
t.Run("group=serve", func(t *testing.T) {
|
|
admin := p.ServeAdmin(ctx)
|
|
assert.Equal(t, "admin.kratos.ory.sh", admin.Host)
|
|
assert.Equal(t, 1234, admin.Port)
|
|
|
|
public := p.ServePublic(ctx)
|
|
assert.Equal(t, "public.kratos.ory.sh", public.Host)
|
|
assert.Equal(t, 1235, public.Port)
|
|
})
|
|
|
|
t.Run("group=dsn", func(t *testing.T) {
|
|
assert.Equal(t, "sqlite://foo.db?mode=memory&_fk=true", p.DSN(ctx))
|
|
})
|
|
|
|
t.Run("group=secrets", func(t *testing.T) {
|
|
assert.Equal(t, [][]byte{
|
|
[]byte("session-key-7f8a9b77-1"),
|
|
[]byte("session-key-7f8a9b77-2"),
|
|
}, p.SecretsSession(ctx))
|
|
var cipherExpected [32]byte
|
|
for k, v := range []byte("secret-thirty-two-character-long") {
|
|
cipherExpected[k] = v
|
|
}
|
|
assert.Equal(t, [][32]byte{
|
|
cipherExpected,
|
|
}, p.SecretsCipher(ctx))
|
|
})
|
|
|
|
t.Run("group=methods", func(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
id string
|
|
config string
|
|
enabled bool
|
|
}{
|
|
{id: "password", enabled: true, config: `{"haveibeenpwned_host":"api.pwnedpasswords.com","haveibeenpwned_enabled":true,"ignore_network_errors":true,"max_breaches":0,"migrate_hook":{"config":{"emit_analytics_event":true,"method":"POST"},"enabled":false},"min_password_length":8,"identifier_similarity_check_enabled":true}`},
|
|
{id: "oidc", enabled: true, config: `{"providers":[{"client_id":"a","client_secret":"b","id":"github","provider":"github","mapper_url":"http://test.kratos.ory.sh/default-identity.schema.json"}]}`},
|
|
{id: "totp", enabled: true, config: `{"issuer":"issuer.ory.sh"}`},
|
|
} {
|
|
strategy := p.SelfServiceStrategy(ctx, tc.id)
|
|
assert.Equal(t, tc.enabled, strategy.Enabled)
|
|
assert.JSONEq(t, tc.config, string(strategy.Config))
|
|
}
|
|
})
|
|
|
|
t.Run("method=registration", func(t *testing.T) {
|
|
assert.Equal(t, true, p.SelfServiceFlowRegistrationEnabled(ctx))
|
|
assert.Equal(t, time.Minute*98, p.SelfServiceFlowRegistrationRequestLifespan(ctx))
|
|
|
|
t.Run("hook=before", func(t *testing.T) {
|
|
expHooks := []config.SelfServiceHook{
|
|
{Name: "web_hook", Config: json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"https://test.kratos.ory.sh/before_registration_hook"}`)},
|
|
{Name: "two_step_registration", Config: json.RawMessage(`{}`)},
|
|
}
|
|
|
|
hooks := p.SelfServiceFlowRegistrationBeforeHooks(ctx)
|
|
|
|
assert.Equal(t, expHooks, hooks)
|
|
// assert.EqualValues(t, "redirect", hook.Name)
|
|
// assert.JSONEq(t, `{"allow_user_defined_redirect":false,"default_redirect_url":"http://test.kratos.ory.sh:4000/"}`, string(hook.Config))
|
|
})
|
|
|
|
for _, tc := range []struct {
|
|
strategy string
|
|
hooks []config.SelfServiceHook
|
|
}{
|
|
{
|
|
strategy: "password",
|
|
hooks: []config.SelfServiceHook{
|
|
{Name: "session", Config: json.RawMessage(`{}`)},
|
|
{Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/after_registration_password_hook"}`)},
|
|
// {Name: "verify", Config: json.RawMessage(`{}`)},
|
|
// {Name: "redirect", Config: json.RawMessage(`{"allow_user_defined_redirect":false,"default_redirect_url":"http://test.kratos.ory.sh:4000/"}`)},
|
|
},
|
|
},
|
|
{
|
|
strategy: "oidc",
|
|
hooks: []config.SelfServiceHook{
|
|
// {Name: "verify", Config: json.RawMessage(`{}`)},
|
|
{Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"GET","url":"https://test.kratos.ory.sh/after_registration_oidc_hook"}`)},
|
|
{Name: "session", Config: json.RawMessage(`{}`)},
|
|
// {Name: "redirect", Config: json.RawMessage(`{"allow_user_defined_redirect":false,"default_redirect_url":"http://test.kratos.ory.sh:4000/"}`)},
|
|
},
|
|
},
|
|
{
|
|
strategy: config.HookGlobal,
|
|
hooks: []config.SelfServiceHook{
|
|
{Name: "web_hook", Config: json.RawMessage(`{"auth":{"config":{"in":"header","name":"My-Key","value":"My-Key-Value"},"type":"api_key"},"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/after_registration_global_hook"}`)},
|
|
},
|
|
},
|
|
} {
|
|
t.Run("hook=after/strategy="+tc.strategy, func(t *testing.T) {
|
|
hooks := p.SelfServiceFlowRegistrationAfterHooks(ctx, tc.strategy)
|
|
assert.Equal(t, tc.hooks, hooks)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("method=totp", func(t *testing.T) {
|
|
assert.Equal(t, "issuer.ory.sh", p.TOTPIssuer(ctx))
|
|
})
|
|
|
|
t.Run("method=login", func(t *testing.T) {
|
|
assert.Equal(t, time.Minute*99, p.SelfServiceFlowLoginRequestLifespan(ctx))
|
|
|
|
t.Run("hook=before", func(t *testing.T) {
|
|
expHooks := []config.SelfServiceHook{
|
|
{Name: "web_hook", Config: json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/before_login_hook"}`)},
|
|
}
|
|
|
|
hooks := p.SelfServiceFlowLoginBeforeHooks(ctx)
|
|
|
|
require.Len(t, hooks, 1)
|
|
assert.Equal(t, expHooks, hooks)
|
|
// assert.EqualValues(t, "redirect", hook.Name)
|
|
// assert.JSONEq(t, `{"allow_user_defined_redirect":false,"default_redirect_url":"http://test.kratos.ory.sh:4000/"}`, string(hook.Config))
|
|
})
|
|
|
|
for _, tc := range []struct {
|
|
strategy string
|
|
hooks []config.SelfServiceHook
|
|
}{
|
|
{
|
|
strategy: "password",
|
|
hooks: []config.SelfServiceHook{
|
|
{Name: "revoke_active_sessions", Config: json.RawMessage(`{}`)},
|
|
{Name: "require_verified_address", Config: json.RawMessage(`{}`)},
|
|
{Name: "web_hook", Config: json.RawMessage(`{"auth":{"config":{"password":"super-secret","user":"test-user"},"type":"basic_auth"},"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/after_login_password_hook"}`)},
|
|
},
|
|
},
|
|
{
|
|
strategy: "oidc",
|
|
hooks: []config.SelfServiceHook{
|
|
{Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"GET","url":"https://test.kratos.ory.sh/after_login_oidc_hook"}`)},
|
|
{Name: "revoke_active_sessions", Config: json.RawMessage(`{}`)},
|
|
},
|
|
},
|
|
{
|
|
strategy: config.HookGlobal,
|
|
hooks: []config.SelfServiceHook{
|
|
{Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/after_login_global_hook"}`)},
|
|
},
|
|
},
|
|
} {
|
|
t.Run("hook=after/strategy="+tc.strategy, func(t *testing.T) {
|
|
hooks := p.SelfServiceFlowLoginAfterHooks(ctx, tc.strategy)
|
|
assert.Equal(t, tc.hooks, hooks)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("method=settings", func(t *testing.T) {
|
|
assert.Equal(t, time.Minute*99, p.SelfServiceFlowSettingsFlowLifespan(ctx))
|
|
assert.Equal(t, time.Minute*5, p.SelfServiceFlowSettingsPrivilegedSessionMaxAge(ctx))
|
|
|
|
for _, tc := range []struct {
|
|
strategy string
|
|
hooks []config.SelfServiceHook
|
|
}{
|
|
{
|
|
strategy: "password",
|
|
hooks: []config.SelfServiceHook{
|
|
{Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/after_settings_password_hook"}`)},
|
|
},
|
|
},
|
|
{
|
|
strategy: "profile",
|
|
hooks: []config.SelfServiceHook{
|
|
{Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/after_settings_profile_hook"}`)},
|
|
},
|
|
},
|
|
{
|
|
strategy: config.HookGlobal,
|
|
hooks: []config.SelfServiceHook{
|
|
{Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/after_settings_global_hook"}`)},
|
|
},
|
|
},
|
|
} {
|
|
t.Run("hook=after/strategy="+tc.strategy, func(t *testing.T) {
|
|
hooks := p.SelfServiceFlowSettingsAfterHooks(ctx, tc.strategy)
|
|
assert.Equal(t, tc.hooks, hooks)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("method=recovery", func(t *testing.T) {
|
|
assert.Equal(t, true, p.SelfServiceFlowRecoveryEnabled(ctx))
|
|
assert.Equal(t, time.Minute*98, p.SelfServiceFlowRecoveryRequestLifespan(ctx))
|
|
assert.Equal(t, "http://test.kratos.ory.sh/recovery", p.SelfServiceFlowRecoveryUI(ctx).String())
|
|
|
|
hooks := p.SelfServiceFlowRecoveryAfterHooks(ctx, config.HookGlobal)
|
|
assert.Equal(t, []config.SelfServiceHook{{Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"GET","url":"https://test.kratos.ory.sh/after_recovery_hook"}`)}}, hooks)
|
|
})
|
|
|
|
t.Run("method=verification", func(t *testing.T) {
|
|
assert.Equal(t, time.Minute*97, p.SelfServiceFlowVerificationRequestLifespan(ctx))
|
|
assert.Equal(t, "http://test.kratos.ory.sh/verification", p.SelfServiceFlowVerificationUI(ctx).String())
|
|
|
|
hooks := p.SelfServiceFlowVerificationAfterHooks(ctx, config.HookGlobal)
|
|
assert.Equal(t, []config.SelfServiceHook{{Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"GET","url":"https://test.kratos.ory.sh/after_verification_hook"}`)}}, hooks)
|
|
})
|
|
|
|
t.Run("group=hashers", func(t *testing.T) {
|
|
c := p.HasherArgon2(ctx)
|
|
assert.Equal(t, &config.Argon2{
|
|
Memory: 1048576, Iterations: 2, Parallelism: 4,
|
|
SaltLength: 16, KeyLength: 32, DedicatedMemory: config.Argon2DefaultDedicatedMemory, ExpectedDeviation: config.Argon2DefaultDeviation, ExpectedDuration: config.Argon2DefaultDuration,
|
|
}, c)
|
|
})
|
|
|
|
t.Run("group=set_provider_by_json", func(t *testing.T) {
|
|
providerConfigJSON := `{"providers": [{"id":"github-test","provider":"github","client_id":"set_json_test","client_secret":"secret","mapper_url":"http://mapper-url","scope":["user:email"]}]}`
|
|
strategyConfigJSON := fmt.Sprintf(`{"enabled":true, "config": %s}`, providerConfigJSON)
|
|
|
|
p.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".oidc", strategyConfigJSON)
|
|
strategy := p.SelfServiceStrategy(ctx, "oidc")
|
|
assert.JSONEq(t, providerConfigJSON, string(strategy.Config))
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestBcrypt(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
p := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.SkipValidation())
|
|
|
|
require.NoError(t, p.Set(ctx, config.ViperKeyHasherBcryptCost, 4))
|
|
require.NoError(t, p.Set(ctx, "dev", false))
|
|
assert.EqualValues(t, uint32(12), p.HasherBcrypt(ctx).Cost)
|
|
|
|
require.NoError(t, p.Set(ctx, "dev", true))
|
|
assert.EqualValues(t, uint32(4), p.HasherBcrypt(ctx).Cost)
|
|
|
|
require.NoError(t, p.Set(ctx, config.ViperKeyHasherBcryptCost, math.MaxInt64)) // too high
|
|
assert.EqualValues(t, math.MaxUint32, p.HasherBcrypt(ctx).Cost)
|
|
}
|
|
|
|
func TestProviderBaseURLs(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
machineHostname, err := os.Hostname()
|
|
if err != nil {
|
|
machineHostname = "127.0.0.1"
|
|
}
|
|
|
|
p := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.SkipValidation())
|
|
assert.Equal(t, "https://"+machineHostname+":4433/", p.SelfPublicURL(ctx).String())
|
|
assert.Equal(t, "https://"+machineHostname+":4434/", p.SelfAdminURL(ctx).String())
|
|
|
|
p = config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.SkipValidation(), configx.WithValues(map[string]interface{}{
|
|
"serve.public.port": 4444,
|
|
"serve.admin.port": 4445,
|
|
}))
|
|
assert.Equal(t, "https://"+machineHostname+":4444/", p.SelfPublicURL(ctx).String())
|
|
assert.Equal(t, "https://"+machineHostname+":4445/", p.SelfAdminURL(ctx).String())
|
|
|
|
p = config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.SkipValidation(), configx.WithValues(map[string]interface{}{
|
|
"serve.public.host": "public.ory.sh",
|
|
"serve.admin.host": "admin.ory.sh",
|
|
"serve.public.port": 4444,
|
|
"serve.admin.port": 4445,
|
|
}))
|
|
assert.Equal(t, "https://public.ory.sh:4444/", p.SelfPublicURL(ctx).String())
|
|
assert.Equal(t, "https://admin.ory.sh:4445/", p.SelfAdminURL(ctx).String())
|
|
|
|
// Set to dev mode
|
|
p = config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.SkipValidation(), configx.WithValues(map[string]interface{}{
|
|
"serve.public.host": "public.ory.sh",
|
|
"serve.admin.host": "admin.ory.sh",
|
|
"serve.public.port": 4444,
|
|
"serve.admin.port": 4445,
|
|
"dev": true,
|
|
}))
|
|
assert.Equal(t, "http://public.ory.sh:4444/", p.SelfPublicURL(ctx).String())
|
|
assert.Equal(t, "http://admin.ory.sh:4445/", p.SelfAdminURL(ctx).String())
|
|
}
|
|
|
|
func TestDefaultWebhookHeaderAllowlist(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
p := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.SkipValidation())
|
|
snapshotx.SnapshotT(t, p.WebhookHeaderAllowlist(ctx))
|
|
}
|
|
|
|
func TestViperProvider_Secrets(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
p := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.SkipValidation())
|
|
|
|
def := p.SecretsDefault(ctx)
|
|
assert.NotEmpty(t, def)
|
|
assert.Equal(t, def, p.SecretsSession(ctx))
|
|
assert.Equal(t, def, p.SecretsDefault(ctx))
|
|
assert.Empty(t, p.SecretsCipher(ctx))
|
|
err := p.Set(ctx, config.ViperKeySecretsCipher, []string{"short-secret-key"})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, [][32]byte{}, p.SecretsCipher(ctx))
|
|
}
|
|
|
|
func TestViperProvider_Defaults(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
l := logrusx.New("", "")
|
|
|
|
for k, tc := range []struct {
|
|
init func() *config.Config
|
|
expect func(t *testing.T, p *config.Config)
|
|
}{
|
|
{
|
|
init: func() *config.Config {
|
|
return config.MustNew(t, l, &contextx.Default{}, configx.SkipValidation())
|
|
},
|
|
},
|
|
{
|
|
init: func() *config.Config {
|
|
return config.MustNew(t, l, &contextx.Default{}, configx.WithConfigFiles("stub/.defaults.yml"), configx.SkipValidation())
|
|
},
|
|
},
|
|
{
|
|
init: func() *config.Config {
|
|
return config.MustNew(t, l, &contextx.Default{}, configx.WithConfigFiles("stub/.defaults-password.yml"), configx.SkipValidation())
|
|
},
|
|
},
|
|
{
|
|
init: func() *config.Config {
|
|
return config.MustNew(t, l, &contextx.Default{}, configx.WithConfigFiles("../../test/e2e/profiles/recovery/.kratos.yml"), configx.SkipValidation())
|
|
},
|
|
expect: func(t *testing.T, p *config.Config) {
|
|
assert.True(t, p.SelfServiceFlowRecoveryEnabled(ctx))
|
|
assert.False(t, p.SelfServiceFlowVerificationEnabled(ctx))
|
|
assert.True(t, p.SelfServiceFlowRegistrationEnabled(ctx))
|
|
assert.True(t, p.SelfServiceStrategy(ctx, "password").Enabled)
|
|
assert.True(t, p.SelfServiceStrategy(ctx, "profile").Enabled)
|
|
assert.True(t, p.SelfServiceStrategy(ctx, "link").Enabled)
|
|
assert.True(t, p.SelfServiceStrategy(ctx, "code").Enabled)
|
|
assert.False(t, p.SelfServiceCodeStrategy(ctx).PasswordlessEnabled)
|
|
assert.False(t, p.SelfServiceStrategy(ctx, "oidc").Enabled)
|
|
},
|
|
},
|
|
{
|
|
init: func() *config.Config {
|
|
return config.MustNew(t, l, &contextx.Default{}, configx.WithConfigFiles("../../test/e2e/profiles/verification/.kratos.yml"), configx.SkipValidation())
|
|
},
|
|
expect: func(t *testing.T, p *config.Config) {
|
|
assert.False(t, p.SelfServiceFlowRecoveryEnabled(ctx))
|
|
assert.True(t, p.SelfServiceFlowVerificationEnabled(ctx))
|
|
assert.True(t, p.SelfServiceFlowRegistrationEnabled(ctx))
|
|
assert.True(t, p.SelfServiceStrategy(ctx, "password").Enabled)
|
|
assert.True(t, p.SelfServiceStrategy(ctx, "profile").Enabled)
|
|
assert.True(t, p.SelfServiceStrategy(ctx, "link").Enabled)
|
|
assert.True(t, p.SelfServiceStrategy(ctx, "code").Enabled)
|
|
assert.False(t, p.SelfServiceCodeStrategy(ctx).PasswordlessEnabled)
|
|
assert.False(t, p.SelfServiceStrategy(ctx, "oidc").Enabled)
|
|
},
|
|
},
|
|
{
|
|
init: func() *config.Config {
|
|
return config.MustNew(t, l, &contextx.Default{}, configx.WithConfigFiles("../../test/e2e/profiles/oidc/.kratos.yml"), configx.SkipValidation())
|
|
},
|
|
expect: func(t *testing.T, p *config.Config) {
|
|
assert.False(t, p.SelfServiceFlowRecoveryEnabled(ctx))
|
|
assert.False(t, p.SelfServiceFlowVerificationEnabled(ctx))
|
|
assert.True(t, p.SelfServiceStrategy(ctx, "password").Enabled)
|
|
assert.True(t, p.SelfServiceStrategy(ctx, "profile").Enabled)
|
|
assert.False(t, p.SelfServiceStrategy(ctx, "link").Enabled)
|
|
assert.True(t, p.SelfServiceStrategy(ctx, "code").Enabled)
|
|
assert.True(t, p.SelfServiceStrategy(ctx, "oidc").Enabled)
|
|
assert.False(t, p.SelfServiceCodeStrategy(ctx).PasswordlessEnabled)
|
|
},
|
|
},
|
|
{
|
|
init: func() *config.Config {
|
|
return config.MustNew(t, l, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.notify-unknown-recipients.yml"), configx.SkipValidation())
|
|
},
|
|
expect: func(t *testing.T, p *config.Config) {
|
|
assert.True(t, p.SelfServiceFlowRecoveryNotifyUnknownRecipients(ctx))
|
|
assert.True(t, p.SelfServiceFlowVerificationNotifyUnknownRecipients(ctx))
|
|
},
|
|
},
|
|
} {
|
|
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
|
|
p := tc.init()
|
|
|
|
if tc.expect != nil {
|
|
tc.expect(t, p)
|
|
return
|
|
}
|
|
assert.False(t, p.SelfServiceFlowRecoveryEnabled(ctx))
|
|
assert.False(t, p.SelfServiceFlowVerificationEnabled(ctx))
|
|
assert.True(t, p.SelfServiceStrategy(ctx, "password").Enabled)
|
|
assert.True(t, p.SelfServiceStrategy(ctx, "profile").Enabled)
|
|
assert.False(t, p.SelfServiceStrategy(ctx, "link").Enabled)
|
|
assert.True(t, p.SelfServiceStrategy(ctx, "code").Enabled)
|
|
assert.False(t, p.SelfServiceStrategy(ctx, "oidc").Enabled)
|
|
assert.False(t, p.SelfServiceCodeStrategy(ctx).PasswordlessEnabled)
|
|
assert.False(t, p.SelfServiceFlowRecoveryNotifyUnknownRecipients(ctx))
|
|
assert.False(t, p.SelfServiceFlowVerificationNotifyUnknownRecipients(ctx))
|
|
})
|
|
}
|
|
|
|
t.Run("suite=ui_url", func(t *testing.T) {
|
|
p := config.MustNew(t, l, &contextx.Default{}, configx.SkipValidation())
|
|
assert.Equal(t, "https://www.ory.com/kratos/docs/fallback/login", p.SelfServiceFlowLoginUI(ctx).String())
|
|
assert.Equal(t, "https://www.ory.com/kratos/docs/fallback/settings", p.SelfServiceFlowSettingsUI(ctx).String())
|
|
assert.Equal(t, "https://www.ory.com/kratos/docs/fallback/registration", p.SelfServiceFlowRegistrationUI(ctx).String())
|
|
assert.Equal(t, "https://www.ory.com/kratos/docs/fallback/recovery", p.SelfServiceFlowRecoveryUI(ctx).String())
|
|
assert.Equal(t, "https://www.ory.com/kratos/docs/fallback/verification", p.SelfServiceFlowVerificationUI(ctx).String())
|
|
})
|
|
}
|
|
|
|
func TestViperProvider_ReturnTo(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
l := logrusx.New("", "")
|
|
p := config.MustNew(t, l, &contextx.Default{}, configx.SkipValidation())
|
|
|
|
p.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, "https://www.ory.com/")
|
|
assert.Equal(t, "https://www.ory.com/", p.SelfServiceFlowVerificationReturnTo(ctx, urlx.ParseOrPanic("https://www.ory.com/")).String())
|
|
assert.Equal(t, "https://www.ory.com/", p.SelfServiceFlowRecoveryReturnTo(ctx, urlx.ParseOrPanic("https://www.ory.com/")).String())
|
|
|
|
p.MustSet(ctx, config.ViperKeySelfServiceRecoveryBrowserDefaultReturnTo, "https://www.ory.com/recovery")
|
|
assert.Equal(t, "https://www.ory.com/recovery", p.SelfServiceFlowRecoveryReturnTo(ctx, urlx.ParseOrPanic("https://www.ory.com/")).String())
|
|
|
|
p.MustSet(ctx, config.ViperKeySelfServiceVerificationBrowserDefaultReturnTo, "https://www.ory.com/verification")
|
|
assert.Equal(t, "https://www.ory.com/verification", p.SelfServiceFlowVerificationReturnTo(ctx, urlx.ParseOrPanic("https://www.ory.com/")).String())
|
|
}
|
|
|
|
func TestSession(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
l := logrusx.New("", "")
|
|
p := config.MustNew(t, l, &contextx.Default{}, configx.SkipValidation())
|
|
|
|
assert.Equal(t, "ory_kratos_session", p.SessionName(ctx))
|
|
p.MustSet(ctx, config.ViperKeySessionName, "ory_session")
|
|
assert.Equal(t, "ory_session", p.SessionName(ctx))
|
|
|
|
assert.Equal(t, time.Hour*24, p.SessionRefreshMinTimeLeft(ctx))
|
|
p.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, "1m")
|
|
assert.Equal(t, time.Minute, p.SessionRefreshMinTimeLeft(ctx))
|
|
|
|
assert.Equal(t, time.Hour*24, p.SessionLifespan(ctx))
|
|
p.MustSet(ctx, config.ViperKeySessionLifespan, "1m")
|
|
assert.Equal(t, time.Minute, p.SessionLifespan(ctx))
|
|
|
|
assert.Equal(t, true, p.SessionPersistentCookie(ctx))
|
|
p.MustSet(ctx, config.ViperKeySessionPersistentCookie, false)
|
|
assert.Equal(t, false, p.SessionPersistentCookie(ctx))
|
|
|
|
assert.Equal(t, false, p.SessionWhoAmICaching(ctx))
|
|
p.MustSet(ctx, config.ViperKeySessionWhoAmICaching, true)
|
|
assert.Equal(t, true, p.SessionWhoAmICaching(ctx))
|
|
}
|
|
|
|
func TestCookies(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
l := logrusx.New("", "")
|
|
p := config.MustNew(t, l, &contextx.Default{}, configx.SkipValidation())
|
|
|
|
t.Run("path", func(t *testing.T) {
|
|
assert.Equal(t, "/", p.CookiePath(ctx))
|
|
assert.Equal(t, "/", p.SessionPath(ctx))
|
|
|
|
p.MustSet(ctx, config.ViperKeyCookiePath, "/cookie")
|
|
assert.Equal(t, "/cookie", p.CookiePath(ctx))
|
|
assert.Equal(t, "/cookie", p.SessionPath(ctx))
|
|
|
|
p.MustSet(ctx, config.ViperKeySessionPath, "/session")
|
|
assert.Equal(t, "/cookie", p.CookiePath(ctx))
|
|
assert.Equal(t, "/session", p.SessionPath(ctx))
|
|
})
|
|
|
|
t.Run("SameSite", func(t *testing.T) {
|
|
assert.Equal(t, http.SameSiteLaxMode, p.CookieSameSiteMode(ctx))
|
|
assert.Equal(t, http.SameSiteLaxMode, p.SessionSameSiteMode(ctx))
|
|
|
|
p.MustSet(ctx, config.ViperKeyCookieSameSite, "Strict")
|
|
assert.Equal(t, http.SameSiteStrictMode, p.CookieSameSiteMode(ctx))
|
|
assert.Equal(t, http.SameSiteStrictMode, p.SessionSameSiteMode(ctx))
|
|
|
|
p.MustSet(ctx, config.ViperKeySessionSameSite, "None")
|
|
assert.Equal(t, http.SameSiteStrictMode, p.CookieSameSiteMode(ctx))
|
|
assert.Equal(t, http.SameSiteNoneMode, p.SessionSameSiteMode(ctx))
|
|
})
|
|
|
|
t.Run("domain", func(t *testing.T) {
|
|
assert.Equal(t, "", p.CookieDomain(ctx))
|
|
assert.Equal(t, "", p.SessionDomain(ctx))
|
|
|
|
p.MustSet(ctx, config.ViperKeyCookieDomain, "www.cookie.com")
|
|
assert.Equal(t, "www.cookie.com", p.CookieDomain(ctx))
|
|
assert.Equal(t, "www.cookie.com", p.SessionDomain(ctx))
|
|
|
|
p.MustSet(ctx, config.ViperKeySessionDomain, "www.session.com")
|
|
assert.Equal(t, "www.cookie.com", p.CookieDomain(ctx))
|
|
assert.Equal(t, "www.session.com", p.SessionDomain(ctx))
|
|
})
|
|
}
|
|
|
|
func TestViperProvider_DSN(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
t.Run("case=dsn: memory", func(t *testing.T) {
|
|
p := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.SkipValidation())
|
|
p.MustSet(ctx, config.ViperKeyDSN, "memory")
|
|
|
|
assert.Equal(t, config.DefaultSQLiteMemoryDSN, p.DSN(ctx))
|
|
})
|
|
|
|
t.Run("case=dsn: not memory", func(t *testing.T) {
|
|
p := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.SkipValidation())
|
|
|
|
dsn := "sqlite://foo.db?_fk=true"
|
|
p.MustSet(ctx, config.ViperKeyDSN, dsn)
|
|
|
|
assert.Equal(t, dsn, p.DSN(ctx))
|
|
})
|
|
|
|
t.Run("case=dsn: not set", func(t *testing.T) {
|
|
dsn := ""
|
|
|
|
var exitCode int
|
|
l := logrusx.New("", "", logrusx.WithExitFunc(func(i int) {
|
|
exitCode = i
|
|
}))
|
|
p := config.MustNew(t, l, &contextx.Default{}, configx.SkipValidation())
|
|
|
|
assert.Equal(t, dsn, p.DSN(ctx))
|
|
assert.NotEqual(t, 0, exitCode)
|
|
})
|
|
}
|
|
|
|
func TestViperProvider_ParseURIOrFail(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
var exitCode int
|
|
|
|
l := logrusx.New("", "", logrusx.WithExitFunc(func(i int) {
|
|
exitCode = i
|
|
}))
|
|
p := config.MustNew(t, l, &contextx.Default{}, configx.SkipValidation())
|
|
require.Zero(t, exitCode)
|
|
|
|
const testKey = "testKeyNotUsedInTheRealSchema"
|
|
|
|
for _, tc := range []struct {
|
|
u string
|
|
expected url.URL
|
|
}{
|
|
{
|
|
u: "file:///etc/config/kratos/identity.schema.json",
|
|
expected: url.URL{
|
|
Scheme: "file",
|
|
Path: "/etc/config/kratos/identity.schema.json",
|
|
},
|
|
},
|
|
{
|
|
u: "file://./identity.schema.json",
|
|
expected: url.URL{
|
|
Scheme: "file",
|
|
Host: ".",
|
|
Path: "/identity.schema.json",
|
|
},
|
|
},
|
|
{
|
|
u: "base64://bG9jYWwgc3ViamVjdCA9I",
|
|
expected: url.URL{
|
|
Scheme: "base64",
|
|
Host: "bG9jYWwgc3ViamVjdCA9I",
|
|
},
|
|
},
|
|
{
|
|
u: "https://foo.bar/schema.json",
|
|
expected: url.URL{
|
|
Scheme: "https",
|
|
Host: "foo.bar",
|
|
Path: "/schema.json",
|
|
},
|
|
},
|
|
} {
|
|
t.Run("case=parse "+tc.u, func(t *testing.T) {
|
|
require.NoError(t, p.Set(ctx, testKey, tc.u))
|
|
|
|
u := p.ParseURIOrFail(ctx, testKey)
|
|
require.Zero(t, exitCode)
|
|
assert.Equal(t, tc.expected, *u)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestViperProvider_HaveIBeenPwned(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
p := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.SkipValidation())
|
|
t.Run("case=hipb: host", func(t *testing.T) {
|
|
p.MustSet(ctx, config.ViperKeyPasswordHaveIBeenPwnedHost, "foo.bar")
|
|
assert.Equal(t, "foo.bar", p.PasswordPolicyConfig(ctx).HaveIBeenPwnedHost)
|
|
})
|
|
|
|
t.Run("case=hibp: enabled", func(t *testing.T) {
|
|
p.MustSet(ctx, config.ViperKeyPasswordHaveIBeenPwnedEnabled, true)
|
|
assert.Equal(t, true, p.PasswordPolicyConfig(ctx).HaveIBeenPwnedEnabled)
|
|
})
|
|
|
|
t.Run("case=hibp: enabled", func(t *testing.T) {
|
|
p.MustSet(ctx, config.ViperKeyPasswordHaveIBeenPwnedEnabled, false)
|
|
assert.Equal(t, false, p.PasswordPolicyConfig(ctx).HaveIBeenPwnedEnabled)
|
|
})
|
|
|
|
t.Run("case=hibp: max_breaches", func(t *testing.T) {
|
|
p.MustSet(ctx, config.ViperKeyPasswordMaxBreaches, 10)
|
|
assert.Equal(t, uint(10), p.PasswordPolicyConfig(ctx).MaxBreaches)
|
|
})
|
|
|
|
t.Run("case=hibp: ignore_network_errors", func(t *testing.T) {
|
|
p.MustSet(ctx, config.ViperKeyIgnoreNetworkErrors, true)
|
|
assert.Equal(t, true, p.PasswordPolicyConfig(ctx).IgnoreNetworkErrors)
|
|
})
|
|
|
|
t.Run("case=hibp: ignore_network_errors", func(t *testing.T) {
|
|
p.MustSet(ctx, config.ViperKeyIgnoreNetworkErrors, false)
|
|
assert.Equal(t, false, p.PasswordPolicyConfig(ctx).IgnoreNetworkErrors)
|
|
})
|
|
}
|
|
|
|
func newTestConfig(t *testing.T, opts ...configx.OptionModifier) (c *config.Config, l *logrusx.Logger, h *test.Hook, exited *bool) {
|
|
l = logrusx.New("", "")
|
|
h = new(test.Hook)
|
|
exited = new(bool)
|
|
l.Logger.Hooks.Add(h)
|
|
l.Logger.ExitFunc = func(code int) { *exited = true }
|
|
c = config.MustNew(t, l, &contextx.Default{}, append([]configx.OptionModifier{configx.SkipValidation()}, opts...)...)
|
|
return
|
|
}
|
|
|
|
func TestLoadingTLSConfig(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
certPath, keyPath, certBase64, keyBase64 := testhelpers.GenerateTLSCertificateFilesForTests(t)
|
|
|
|
t.Run("case=public: no TLS config", func(t *testing.T) {
|
|
p, l, hook, exited := newTestConfig(t)
|
|
certFunc, err := p.ServePublic(t.Context()).TLS.GetCertFunc(t.Context(), l, "public")
|
|
require.NoError(t, err)
|
|
assert.Nil(t, certFunc)
|
|
le := hook.LastEntry()
|
|
require.NotNil(t, le)
|
|
assert.Equal(t, "TLS has not been configured for public, skipping", le.Message)
|
|
assert.False(t, *exited)
|
|
})
|
|
|
|
t.Run("case=admin: no TLS config", func(t *testing.T) {
|
|
p, l, hook, exited := newTestConfig(t)
|
|
certFunc, err := p.ServeAdmin(t.Context()).TLS.GetCertFunc(t.Context(), l, "admin")
|
|
require.NoError(t, err)
|
|
assert.Nil(t, certFunc)
|
|
le := hook.LastEntry()
|
|
require.NotNil(t, le)
|
|
assert.Equal(t, "TLS has not been configured for admin, skipping", le.Message)
|
|
assert.False(t, *exited)
|
|
})
|
|
|
|
t.Run("case=public: loading inline base64 certificate", func(t *testing.T) {
|
|
p, l, hook, exited := newTestConfig(t, configx.WithValues(map[string]interface{}{
|
|
keyPublicTLSKeyBase64: keyBase64,
|
|
keyPublicTLSCertBase64: certBase64,
|
|
}))
|
|
certFunc, err := p.ServePublic(t.Context()).TLS.GetCertFunc(t.Context(), l, "public")
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, certFunc)
|
|
le := hook.LastEntry()
|
|
require.NotNil(t, le)
|
|
assert.Equal(t, "Setting up HTTPS for public", le.Message)
|
|
assert.False(t, *exited)
|
|
})
|
|
|
|
t.Run("case=public: loading certificate from a file", func(t *testing.T) {
|
|
p, l, hook, exited := newTestConfig(t, configx.WithValues(map[string]interface{}{
|
|
keyPublicTLSKeyPath: keyPath,
|
|
keyPublicTLSCertPath: certPath,
|
|
}))
|
|
certFunc, err := p.ServePublic(t.Context()).TLS.GetCertFunc(t.Context(), l, "public")
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, certFunc)
|
|
le := hook.LastEntry()
|
|
require.NotNil(t, le)
|
|
assert.Equal(t, "Setting up HTTPS for public (automatic certificate reloading active)", le.Message)
|
|
assert.False(t, *exited)
|
|
})
|
|
|
|
t.Run("case=public: failing to load inline base64 certificate", func(t *testing.T) {
|
|
p, l, _, _ := newTestConfig(t, configx.WithValues(map[string]interface{}{
|
|
keyPublicTLSKeyBase64: "invalid",
|
|
keyPublicTLSCertBase64: certBase64,
|
|
}))
|
|
certFunc, err := p.ServePublic(t.Context()).TLS.GetCertFunc(t.Context(), l, "public")
|
|
require.ErrorContains(t, err, "unable to load TLS certificate for interface public")
|
|
assert.Nil(t, certFunc)
|
|
})
|
|
|
|
t.Run("case=public: failing to load certificate from a file", func(t *testing.T) {
|
|
p, l, _, _ := newTestConfig(t, configx.WithValues(map[string]interface{}{
|
|
keyPublicTLSKeyPath: "/dev/null",
|
|
keyPublicTLSCertPath: "/dev/null",
|
|
}))
|
|
certFunc, err := p.ServePublic(t.Context()).TLS.GetCertFunc(t.Context(), l, "public")
|
|
require.ErrorContains(t, err, "unable to load TLS certificate for interface public")
|
|
assert.Nil(t, certFunc)
|
|
})
|
|
|
|
t.Run("case=admin: loading inline base64 certificate", func(t *testing.T) {
|
|
p, l, hook, exited := newTestConfig(t, configx.WithValues(map[string]interface{}{
|
|
keyAdminTLSKeyBase64: keyBase64,
|
|
keyAdminTLSCertBase64: certBase64,
|
|
}))
|
|
certFunc, err := p.ServeAdmin(t.Context()).TLS.GetCertFunc(t.Context(), l, "admin")
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, certFunc)
|
|
le := hook.LastEntry()
|
|
require.NotNil(t, le)
|
|
assert.Equal(t, "Setting up HTTPS for admin", le.Message)
|
|
assert.False(t, *exited)
|
|
})
|
|
|
|
t.Run("case=admin: loading certificate from a file", func(t *testing.T) {
|
|
p, l, hook, exited := newTestConfig(t, configx.WithValues(map[string]interface{}{
|
|
keyAdminTLSKeyPath: keyPath,
|
|
keyAdminTLSCertPath: certPath,
|
|
}))
|
|
certFunc, err := p.ServeAdmin(t.Context()).TLS.GetCertFunc(t.Context(), l, "admin")
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, certFunc)
|
|
le := hook.LastEntry()
|
|
require.NotNil(t, le)
|
|
assert.Equal(t, "Setting up HTTPS for admin (automatic certificate reloading active)", le.Message)
|
|
assert.False(t, *exited)
|
|
})
|
|
|
|
t.Run("case=admin: failing to load inline base64 certificate", func(t *testing.T) {
|
|
p, l, _, _ := newTestConfig(t, configx.WithValues(map[string]interface{}{
|
|
keyAdminTLSKeyBase64: "invalid",
|
|
keyAdminTLSCertBase64: certBase64,
|
|
}))
|
|
certFunc, err := p.ServeAdmin(t.Context()).TLS.GetCertFunc(t.Context(), l, "admin")
|
|
assert.Nil(t, certFunc)
|
|
require.ErrorContains(t, err, "unable to load TLS certificate for interface admin")
|
|
})
|
|
|
|
t.Run("case=admin: failing to load certificate from a file", func(t *testing.T) {
|
|
p, l, _, _ := newTestConfig(t, configx.WithValues(map[string]interface{}{
|
|
keyAdminTLSKeyPath: "/dev/null",
|
|
keyAdminTLSCertPath: certPath,
|
|
}))
|
|
certFunc, err := p.ServeAdmin(t.Context()).TLS.GetCertFunc(t.Context(), l, "admin")
|
|
require.ErrorContains(t, err, "unable to load TLS certificate for interface admin")
|
|
assert.Nil(t, certFunc)
|
|
})
|
|
}
|
|
|
|
func TestIdentitySchemaValidation(t *testing.T) {
|
|
t.Parallel()
|
|
files := []string{"stub/.identity.test.json", "stub/.identity.other.json"}
|
|
|
|
ctx := context.Background()
|
|
ctx = config.SetValidateIdentitySchemaResilientClientOptions(ctx, []httpx.ResilientOptions{
|
|
httpx.ResilientClientWithMaxRetry(0),
|
|
httpx.ResilientClientWithConnectionTimeout(time.Millisecond * 100),
|
|
})
|
|
|
|
type identity struct {
|
|
Schemas []map[string]string `json:"schemas"`
|
|
}
|
|
|
|
type configFile struct {
|
|
identityFileName string
|
|
SelfService map[string]string `json:"selfservice"`
|
|
Courier map[string]map[string]string `json:"courier"`
|
|
DSN string `json:"dsn"`
|
|
Identity *identity `json:"identity"`
|
|
}
|
|
|
|
setup := func(t *testing.T, file string) *configFile {
|
|
identityTest, err := os.ReadFile(file) // #nosec G304 -- test code
|
|
assert.NoError(t, err)
|
|
return &configFile{
|
|
identityFileName: file,
|
|
SelfService: map[string]string{
|
|
"default_browser_return_url": "https://some-return-url",
|
|
},
|
|
Courier: map[string]map[string]string{
|
|
"smtp": {
|
|
"connection_uri": "smtp://foo@bar",
|
|
},
|
|
},
|
|
DSN: "memory",
|
|
Identity: &identity{
|
|
Schemas: []map[string]string{{"id": "default", "url": "base64://" + base64.StdEncoding.EncodeToString(identityTest)}},
|
|
},
|
|
}
|
|
}
|
|
|
|
marshalAndWrite := func(t *testing.T, tmpFile *os.File, identity *configFile) {
|
|
j, err := yaml.Marshal(identity)
|
|
assert.NoError(t, err)
|
|
|
|
_, err = tmpFile.Seek(0, 0)
|
|
require.NoError(t, err)
|
|
require.NoError(t, tmpFile.Truncate(0))
|
|
_, err = io.Writer.Write(tmpFile, j)
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, tmpFile.Sync())
|
|
}
|
|
|
|
testWatch := func(t *testing.T, ctx context.Context, identity *configFile) (*config.Config, *test.Hook, func([]map[string]string)) {
|
|
tdir := t.TempDir()
|
|
assert.NoError(t, os.MkdirAll(tdir, 0o750))
|
|
configFileName := randx.MustString(8, randx.Alpha)
|
|
tmpConfig, err := os.Create(filepath.Join(tdir, configFileName+".config.yaml")) // #nosec G304 -- test code
|
|
assert.NoError(t, err)
|
|
t.Cleanup(func() { _ = tmpConfig.Close() })
|
|
|
|
marshalAndWrite(t, tmpConfig, identity)
|
|
|
|
l := logrusx.New("kratos-"+tmpConfig.Name(), "test")
|
|
hook := test.NewLocal(l.Logger)
|
|
|
|
conf, err := config.New(ctx, l, os.Stderr, &contextx.Default{}, configx.WithConfigFiles(tmpConfig.Name()))
|
|
assert.NoError(t, err)
|
|
|
|
// clean the hooks since it will throw an event on first boot
|
|
hook.Reset()
|
|
|
|
return conf, hook, func(schemas []map[string]string) {
|
|
identity.Identity.Schemas = schemas
|
|
marshalAndWrite(t, tmpConfig, identity)
|
|
}
|
|
}
|
|
|
|
t.Run("case=skip invalid schema validation", func(t *testing.T) {
|
|
_, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
|
|
configx.WithConfigFiles("stub/.kratos.invalid.identities.yaml"),
|
|
configx.SkipValidation())
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("case=invalid schema should throw error", func(t *testing.T) {
|
|
var stdErr bytes.Buffer
|
|
_, err := config.New(ctx, logrusx.New("", ""), &stdErr, &contextx.Default{},
|
|
configx.WithConfigFiles("stub/.kratos.invalid.identities.yaml"))
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "minimum 1 properties allowed, but found 0")
|
|
assert.Contains(t, stdErr.String(), "minimum 1 properties allowed, but found 0")
|
|
})
|
|
|
|
t.Run("case=must fail on loading unreachable schemas", func(t *testing.T) {
|
|
// we make sure that the test runs into DNS issues instead of the context being canceled
|
|
ctx := config.SetValidateIdentitySchemaResilientClientOptions(ctx, []httpx.ResilientOptions{
|
|
httpx.ResilientClientWithMaxRetry(0),
|
|
httpx.ResilientClientWithConnectionTimeout(5 * time.Second),
|
|
})
|
|
|
|
err := make(chan error)
|
|
go func(err chan error) {
|
|
_, e := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
|
|
configx.WithConfigFiles("stub/.kratos.mock.identities.yaml"))
|
|
err <- e
|
|
}(err)
|
|
|
|
select {
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("the test could not complete as the context timed out before the identity schema loader timed out")
|
|
case e := <-err:
|
|
assert.ErrorContains(t, e, "no such host")
|
|
}
|
|
})
|
|
|
|
t.Run("case=validate schema is validated on file change", func(t *testing.T) {
|
|
var identities []*configFile
|
|
|
|
for _, f := range files {
|
|
identities = append(identities, setup(t, f))
|
|
}
|
|
|
|
invalidIdentity := setup(t, "stub/.identity.invalid.json")
|
|
|
|
for _, identity := range identities {
|
|
t.Run("test=identity file "+identity.identityFileName, func(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(ctx, time.Second*30)
|
|
t.Cleanup(cancel)
|
|
|
|
_, hook, writeSchema := testWatch(t, ctx, identity)
|
|
writeSchema(invalidIdentity.Identity.Schemas)
|
|
|
|
// There are a bunch of log messages beeing logged. We are looking for a specific one.
|
|
for {
|
|
for _, v := range hook.AllEntries() {
|
|
s, err := v.String()
|
|
require.NoError(t, err)
|
|
if strings.Contains(s, "The changed identity schema configuration is invalid and could not be loaded.") {
|
|
return
|
|
}
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
t.Fatal("the test could not complete as the context timed out before the file watcher updated")
|
|
default: // nothing
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestPasswordless(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
|
|
configx.SkipValidation(),
|
|
configx.WithValue(config.ViperKeyWebAuthnPasswordless, true))
|
|
require.NoError(t, err)
|
|
|
|
assert.True(t, conf.WebAuthnForPasswordless(ctx))
|
|
conf.MustSet(ctx, config.ViperKeyWebAuthnPasswordless, false)
|
|
assert.False(t, conf.WebAuthnForPasswordless(ctx))
|
|
}
|
|
|
|
func TestPasswordlessCode(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
|
|
conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
|
|
configx.SkipValidation(),
|
|
configx.WithValue(config.ViperKeySelfServiceStrategyConfig+".code", map[string]interface{}{
|
|
"passwordless_enabled": true,
|
|
"passwordless_login_fallback_enabled": true,
|
|
"config": map[string]interface{}{},
|
|
}))
|
|
require.NoError(t, err)
|
|
|
|
assert.True(t, conf.SelfServiceCodeStrategy(ctx).PasswordlessEnabled)
|
|
}
|
|
|
|
func TestChangeMinPasswordLength(t *testing.T) {
|
|
t.Parallel()
|
|
t.Run("case=must fail on minimum password length below enforced minimum", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
_, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
|
|
configx.WithConfigFiles("stub/.kratos.yaml"),
|
|
configx.WithValue(config.ViperKeyPasswordMinLength, 5))
|
|
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("case=must not fail on minimum password length above enforced minimum", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
_, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
|
|
configx.WithConfigFiles("stub/.kratos.yaml"),
|
|
configx.WithValue(config.ViperKeyPasswordMinLength, 9))
|
|
|
|
assert.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestCourierEmailHTTP(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
t.Run("case=configs set", func(t *testing.T) {
|
|
conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
|
|
configx.WithConfigFiles("stub/.kratos.courier.email.http.yaml"), configx.SkipValidation())
|
|
assert.Equal(t, "http", conf.CourierEmailStrategy(ctx))
|
|
snapshotx.SnapshotT(t, conf.CourierEmailRequestConfig(ctx))
|
|
})
|
|
|
|
t.Run("case=defaults", func(t *testing.T) {
|
|
conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation())
|
|
|
|
assert.Equal(t, "smtp", conf.CourierEmailStrategy(ctx))
|
|
})
|
|
}
|
|
|
|
func TestCourierChannels(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
t.Run("case=configs set", func(t *testing.T) {
|
|
conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.courier.channels.yaml"), configx.SkipValidation())
|
|
|
|
channelConfig, err := conf.CourierChannels(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, channelConfig, 2)
|
|
assert.Equal(t, channelConfig[0].ID, "phone")
|
|
assert.NotEmpty(t, channelConfig[0].RequestConfig)
|
|
assert.Equal(t, channelConfig[1].ID, "email")
|
|
assert.NotEmpty(t, channelConfig[1].SMTPConfig)
|
|
})
|
|
|
|
t.Run("case=defaults", func(t *testing.T) {
|
|
conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation())
|
|
|
|
channelConfig, err := conf.CourierChannels(ctx)
|
|
require.NoError(t, err)
|
|
assert.Len(t, channelConfig, 1)
|
|
assert.Equal(t, channelConfig[0].ID, "email")
|
|
assert.Equal(t, channelConfig[0].Type, "smtp")
|
|
})
|
|
|
|
t.Run("smtp urls", func(t *testing.T) {
|
|
for _, tc := range []string{
|
|
"smtp://a:basdasdasda%2Fc@email-smtp.eu-west-3.amazonaws.com:587/",
|
|
"smtp://a:b$c@email-smtp.eu-west-3.amazonaws.com:587/",
|
|
"smtp://a/a:bc@email-smtp.eu-west-3.amazonaws.com:587",
|
|
"smtp://aa:b+c@email-smtp.eu-west-3.amazonaws.com:587/",
|
|
"smtp://user?name:password@email-smtp.eu-west-3.amazonaws.com:587/",
|
|
"smtp://username:pass%2Fword@email-smtp.eu-west-3.amazonaws.com:587/",
|
|
} {
|
|
t.Run("case="+tc, func(t *testing.T) {
|
|
conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithValue(config.ViperKeyCourierSMTPURL, tc), configx.SkipValidation())
|
|
require.NoError(t, err)
|
|
cs, err := conf.CourierChannels(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, cs, 1)
|
|
assert.Equal(t, tc, cs[0].SMTPConfig.ConnectionURI)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCourierMessageTTL(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
t.Run("case=configs set", func(t *testing.T) {
|
|
conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
|
|
configx.WithConfigFiles("stub/.kratos.courier.message_retries.yaml"), configx.SkipValidation())
|
|
assert.Equal(t, conf.CourierMessageRetries(ctx), 10)
|
|
})
|
|
|
|
t.Run("case=defaults", func(t *testing.T) {
|
|
conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation())
|
|
assert.Equal(t, conf.CourierMessageRetries(ctx), 5)
|
|
})
|
|
}
|
|
|
|
func TestTwoStep(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
t.Run("case=nothing is set", func(t *testing.T) {
|
|
conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation())
|
|
|
|
assert.True(t, conf.SelfServiceFlowRegistrationTwoSteps(ctx))
|
|
})
|
|
|
|
t.Run("case=legacy config explicit off", func(t *testing.T) {
|
|
conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
|
|
configx.WithValue(config.ViperKeySelfServiceRegistrationEnableLegacyOneStep, false),
|
|
configx.SkipValidation(),
|
|
)
|
|
|
|
assert.True(t, conf.SelfServiceFlowRegistrationTwoSteps(ctx))
|
|
})
|
|
|
|
t.Run("case=legacy config explicit on", func(t *testing.T) {
|
|
conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
|
|
configx.WithValue(config.ViperKeySelfServiceRegistrationEnableLegacyOneStep, true),
|
|
configx.SkipValidation(),
|
|
)
|
|
|
|
assert.False(t, conf.SelfServiceFlowRegistrationTwoSteps(ctx))
|
|
})
|
|
|
|
t.Run("case=new config explicit on", func(t *testing.T) {
|
|
conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
|
|
configx.WithValue(config.ViperKeySelfServiceRegistrationFlowStyle, "profile_first"),
|
|
configx.SkipValidation(),
|
|
)
|
|
|
|
assert.True(t, conf.SelfServiceFlowRegistrationTwoSteps(ctx))
|
|
})
|
|
|
|
t.Run("case=new config explicit off", func(t *testing.T) {
|
|
conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
|
|
configx.WithValue(config.ViperKeySelfServiceRegistrationFlowStyle, "unified"),
|
|
configx.SkipValidation(),
|
|
)
|
|
|
|
assert.False(t, conf.SelfServiceFlowRegistrationTwoSteps(ctx))
|
|
})
|
|
|
|
t.Run("case=new config explicit on but legacy off", func(t *testing.T) {
|
|
conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
|
|
configx.WithValue(config.ViperKeySelfServiceRegistrationFlowStyle, "profile_first"),
|
|
configx.WithValue(config.ViperKeySelfServiceRegistrationEnableLegacyOneStep, true),
|
|
configx.SkipValidation(),
|
|
)
|
|
|
|
assert.False(t, conf.SelfServiceFlowRegistrationTwoSteps(ctx))
|
|
})
|
|
}
|
|
|
|
func TestOAuth2Provider(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
t.Run("case=configs set", func(t *testing.T) {
|
|
conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
|
|
configx.WithConfigFiles("stub/.kratos.oauth2_provider.yaml"), configx.SkipValidation())
|
|
assert.Equal(t, "https://oauth2_provider/", conf.OAuth2ProviderURL(ctx).String())
|
|
assert.Equal(t, http.Header{"Authorization": {"Basic"}}, conf.OAuth2ProviderHeader(ctx))
|
|
assert.True(t, conf.OAuth2ProviderOverrideReturnTo(ctx))
|
|
})
|
|
|
|
t.Run("case=defaults", func(t *testing.T) {
|
|
conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation())
|
|
assert.Empty(t, conf.OAuth2ProviderURL(ctx))
|
|
assert.Empty(t, conf.OAuth2ProviderHeader(ctx))
|
|
assert.False(t, conf.OAuth2ProviderOverrideReturnTo(ctx))
|
|
})
|
|
}
|
|
|
|
func TestWebauthn(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
t.Run("case=multiple origins", func(t *testing.T) {
|
|
conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
|
|
configx.WithConfigFiles("stub/.kratos.webauthn.origins.yaml"))
|
|
require.NoError(t, err)
|
|
webAuthnConfig := conf.WebAuthnConfig(ctx)
|
|
assert.Equal(t, "https://example.com/webauthn", webAuthnConfig.RPID)
|
|
assert.EqualValues(t, []string{
|
|
"https://origin-a.example.com",
|
|
"https://origin-b.example.com",
|
|
"https://origin-c.example.com",
|
|
}, webAuthnConfig.RPOrigins)
|
|
})
|
|
|
|
t.Run("case=one origin", func(t *testing.T) {
|
|
conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
|
|
configx.WithConfigFiles("stub/.kratos.webauthn.origin.yaml"))
|
|
require.NoError(t, err)
|
|
webAuthnConfig := conf.WebAuthnConfig(ctx)
|
|
assert.Equal(t, "https://example.com/webauthn", webAuthnConfig.RPID)
|
|
assert.EqualValues(t, []string{
|
|
"https://origin-a.example.com",
|
|
}, webAuthnConfig.RPOrigins)
|
|
})
|
|
|
|
t.Run("case=id as origin", func(t *testing.T) {
|
|
conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
|
|
configx.WithConfigFiles("stub/.kratos.yaml"))
|
|
require.NoError(t, err)
|
|
webAuthnConfig := conf.WebAuthnConfig(ctx)
|
|
assert.Equal(t, "example.com", webAuthnConfig.RPID)
|
|
assert.EqualValues(t, []string{
|
|
"http://example.com",
|
|
}, webAuthnConfig.RPOrigins)
|
|
})
|
|
|
|
t.Run("case=invalid", func(t *testing.T) {
|
|
_, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
|
|
configx.WithConfigFiles("stub/.kratos.webauthn.invalid.yaml"))
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestPasskeyConfig(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
t.Run("case=defaults", func(t *testing.T) {
|
|
conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
|
|
configx.WithConfigFiles("stub/.kratos.passkey.yaml"))
|
|
require.NoError(t, err)
|
|
|
|
c := conf.PasskeyConfig(ctx)
|
|
// Attachment is unset by default so users can register either
|
|
// platform or cross-platform authenticators.
|
|
assert.Empty(t, c.AuthenticatorSelection.AuthenticatorAttachment)
|
|
require.NotNil(t, c.AuthenticatorSelection.RequireResidentKey)
|
|
assert.True(t, *c.AuthenticatorSelection.RequireResidentKey)
|
|
assert.Equal(t, protocol.ResidentKeyRequirementRequired, c.AuthenticatorSelection.ResidentKey)
|
|
assert.Equal(t, protocol.VerificationPreferred, c.AuthenticatorSelection.UserVerification)
|
|
assert.Equal(t, protocol.PreferNoAttestation, c.AttestationPreference)
|
|
// Timeouts should be zero (use library defaults) when not configured
|
|
assert.Equal(t, time.Duration(0), c.Timeouts.Registration.Timeout)
|
|
assert.Equal(t, time.Duration(0), c.Timeouts.Login.Timeout)
|
|
})
|
|
|
|
t.Run("case=reads overrides from config", func(t *testing.T) {
|
|
conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
|
|
configx.WithConfigFiles("stub/.kratos.passkey.options.yaml"))
|
|
require.NoError(t, err)
|
|
|
|
c := conf.PasskeyConfig(ctx)
|
|
assert.Equal(t, protocol.CrossPlatform, c.AuthenticatorSelection.AuthenticatorAttachment)
|
|
require.NotNil(t, c.AuthenticatorSelection.RequireResidentKey)
|
|
assert.False(t, *c.AuthenticatorSelection.RequireResidentKey)
|
|
assert.Equal(t, protocol.ResidentKeyRequirementDiscouraged, c.AuthenticatorSelection.ResidentKey)
|
|
assert.Equal(t, protocol.VerificationRequired, c.AuthenticatorSelection.UserVerification)
|
|
assert.Equal(t, protocol.PreferDirectAttestation, c.AttestationPreference)
|
|
assert.Equal(t, 30*time.Second, c.Timeouts.Registration.Timeout)
|
|
assert.Equal(t, 30*time.Second, c.Timeouts.Registration.TimeoutUVD)
|
|
assert.Equal(t, 45*time.Second, c.Timeouts.Login.Timeout)
|
|
assert.Equal(t, 45*time.Second, c.Timeouts.Login.TimeoutUVD)
|
|
})
|
|
}
|
|
|
|
func TestCourierTemplatesConfig(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
t.Run("case=partial template update allowed", func(t *testing.T) {
|
|
_, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
|
|
configx.WithConfigFiles("stub/.kratos.courier.remote.partial.templates.yaml"))
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("case=load remote template with fallback template overrides path", func(t *testing.T) {
|
|
_, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
|
|
configx.WithConfigFiles("stub/.kratos.courier.remote.templates.yaml"))
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("case=courier template helper", func(t *testing.T) {
|
|
c, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
|
|
configx.WithConfigFiles("stub/.kratos.courier.remote.templates.yaml"))
|
|
|
|
assert.NoError(t, err)
|
|
|
|
courierTemplateConfig := &config.CourierEmailTemplate{
|
|
Body: &config.CourierEmailBodyTemplate{
|
|
PlainText: "",
|
|
HTML: "",
|
|
},
|
|
Subject: "",
|
|
}
|
|
|
|
assert.Equal(t, courierTemplateConfig, c.CourierEmailTemplatesHelper(ctx, config.ViperKeyCourierTemplatesVerificationInvalidEmail))
|
|
assert.Equal(t, courierTemplateConfig, c.CourierEmailTemplatesHelper(ctx, config.ViperKeyCourierTemplatesVerificationValidEmail))
|
|
// this should return an empty courierEmailTemplate as the key does not exist
|
|
assert.Equal(t, courierTemplateConfig, c.CourierEmailTemplatesHelper(ctx, "a_random_key"))
|
|
|
|
courierTemplateConfig = &config.CourierEmailTemplate{
|
|
Body: &config.CourierEmailBodyTemplate{
|
|
PlainText: "base64://SGksCgp5b3UgKG9yIHNvbWVvbmUgZWxzZSkgZW50ZXJlZCB0aGlzIGVtYWlsIGFkZHJlc3Mgd2hlbiB0cnlpbmcgdG8gcmVjb3ZlciBhY2Nlc3MgdG8gYW4gYWNjb3VudC4KCkhvd2V2ZXIsIHRoaXMgZW1haWwgYWRkcmVzcyBpcyBub3Qgb24gb3VyIGRhdGFiYXNlIG9mIHJlZ2lzdGVyZWQgdXNlcnMgYW5kIHRoZXJlZm9yZSB0aGUgYXR0ZW1wdCBoYXMgZmFpbGVkLgoKSWYgdGhpcyB3YXMgeW91LCBjaGVjayBpZiB5b3Ugc2lnbmVkIHVwIHVzaW5nIGEgZGlmZmVyZW50IGFkZHJlc3MuCgpJZiB0aGlzIHdhcyBub3QgeW91LCBwbGVhc2UgaWdub3JlIHRoaXMgZW1haWwu",
|
|
HTML: "base64://SGksCgp5b3UgKG9yIHNvbWVvbmUgZWxzZSkgZW50ZXJlZCB0aGlzIGVtYWlsIGFkZHJlc3Mgd2hlbiB0cnlpbmcgdG8gcmVjb3ZlciBhY2Nlc3MgdG8gYW4gYWNjb3VudC4KCkhvd2V2ZXIsIHRoaXMgZW1haWwgYWRkcmVzcyBpcyBub3Qgb24gb3VyIGRhdGFiYXNlIG9mIHJlZ2lzdGVyZWQgdXNlcnMgYW5kIHRoZXJlZm9yZSB0aGUgYXR0ZW1wdCBoYXMgZmFpbGVkLgoKSWYgdGhpcyB3YXMgeW91LCBjaGVjayBpZiB5b3Ugc2lnbmVkIHVwIHVzaW5nIGEgZGlmZmVyZW50IGFkZHJlc3MuCgpJZiB0aGlzIHdhcyBub3QgeW91LCBwbGVhc2UgaWdub3JlIHRoaXMgZW1haWwu",
|
|
},
|
|
Subject: "base64://QWNjb3VudCBBY2Nlc3MgQXR0ZW1wdGVk",
|
|
}
|
|
assert.Equal(t, courierTemplateConfig, c.CourierEmailTemplatesHelper(ctx, config.ViperKeyCourierTemplatesRecoveryInvalidEmail))
|
|
|
|
courierTemplateConfig = &config.CourierEmailTemplate{
|
|
Body: &config.CourierEmailBodyTemplate{
|
|
PlainText: "base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQp7eyAuUmVjb3ZlcnlVUkwgfX0K",
|
|
HTML: "base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQo8YSBocmVmPSJ7eyAuUmVjb3ZlcnlVUkwgfX0iPnt7IC5SZWNvdmVyeVVSTCB9fTwvYT4=",
|
|
},
|
|
Subject: "base64://UmVjb3ZlciBhY2Nlc3MgdG8geW91ciBhY2NvdW50",
|
|
}
|
|
assert.Equal(t, courierTemplateConfig, c.CourierEmailTemplatesHelper(ctx, config.ViperKeyCourierTemplatesRecoveryValidEmail))
|
|
})
|
|
}
|
|
|
|
func TestCleanup(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
p := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.yaml"))
|
|
|
|
t.Run("group=cleanup config", func(t *testing.T) {
|
|
assert.Equal(t, p.DatabaseCleanupSleepTables(ctx), 1*time.Minute)
|
|
p.MustSet(ctx, config.ViperKeyDatabaseCleanupSleepTables, "1s")
|
|
assert.Equal(t, p.DatabaseCleanupSleepTables(ctx), time.Second)
|
|
assert.Equal(t, p.DatabaseCleanupBatchSize(ctx), 100)
|
|
p.MustSet(ctx, config.ViperKeyDatabaseCleanupBatchSize, "1")
|
|
assert.Equal(t, p.DatabaseCleanupBatchSize(ctx), 1)
|
|
})
|
|
}
|
|
|
|
func TestOrganizationConfigSessionLifespan(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := t.Context()
|
|
orgID := uuid.Must(uuid.NewV4())
|
|
conf := config.MustNew(t, logrusx.New("", ""), &contextx.Default{},
|
|
configx.WithValues(map[string]interface{}{
|
|
"selfservice.methods.b2b.config.organizations": []map[string]interface{}{
|
|
{
|
|
"id": orgID.String(),
|
|
"domains": []string{"example.com"},
|
|
"session_lifespan": "2h",
|
|
},
|
|
},
|
|
}),
|
|
configx.SkipValidation(),
|
|
)
|
|
|
|
orgs := conf.Organizations(ctx)
|
|
require.Len(t, orgs, 1)
|
|
assert.Equal(t, orgID, orgs[0].ID)
|
|
assert.Equal(t, 2*time.Hour, orgs[0].SessionLifespan)
|
|
}
|
|
|
|
func TestConfigOrganizationSessionLifespan(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := t.Context()
|
|
orgWithOverride := uuid.Must(uuid.NewV4())
|
|
orgWithoutOverride := uuid.Must(uuid.NewV4())
|
|
|
|
conf := config.MustNew(t, logrusx.New("", ""), &contextx.Default{},
|
|
configx.WithValues(map[string]interface{}{
|
|
"session.lifespan": "24h",
|
|
"selfservice.methods.b2b.config.organizations": []map[string]interface{}{
|
|
{"id": orgWithOverride.String(), "domains": []string{"a.com"}, "session_lifespan": "1h"},
|
|
{"id": orgWithoutOverride.String(), "domains": []string{"b.com"}},
|
|
},
|
|
}),
|
|
configx.SkipValidation(),
|
|
)
|
|
|
|
t.Run("case=nil orgID returns project default", func(t *testing.T) {
|
|
t.Parallel()
|
|
assert.Equal(t, 24*time.Hour, conf.OrganizationSessionLifespan(ctx, uuid.Nil))
|
|
})
|
|
|
|
t.Run("case=org with override returns override", func(t *testing.T) {
|
|
t.Parallel()
|
|
assert.Equal(t, 1*time.Hour, conf.OrganizationSessionLifespan(ctx, orgWithOverride))
|
|
})
|
|
|
|
t.Run("case=org without override returns project default", func(t *testing.T) {
|
|
t.Parallel()
|
|
assert.Equal(t, 24*time.Hour, conf.OrganizationSessionLifespan(ctx, orgWithoutOverride))
|
|
})
|
|
|
|
t.Run("case=unknown orgID returns project default", func(t *testing.T) {
|
|
t.Parallel()
|
|
assert.Equal(t, 24*time.Hour, conf.OrganizationSessionLifespan(ctx, uuid.Must(uuid.NewV4())))
|
|
})
|
|
}
|
|
|
|
func TestSelfServiceBrowserAllowedReturnToDomains(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
entry string
|
|
allowed bool
|
|
}{
|
|
{name: "case=exact domain is kept", entry: "https://foo.com", allowed: true},
|
|
{name: "case=labelled wildcard is kept", entry: "https://*.foo.com", allowed: true},
|
|
{name: "case=path wildcard is kept", entry: "https://app.foo.com/*", allowed: true},
|
|
{name: "case=bare wildcard is rejected", entry: "https://*", allowed: false},
|
|
{name: "case=eTLD wildcard is rejected", entry: "https://*.com", allowed: false},
|
|
{name: "case=multi-label eTLD is rejected", entry: "https://*.co.uk", allowed: false},
|
|
{name: "case=dot-less wildcard is rejected", entry: "https://*foo.com", allowed: false},
|
|
{name: "case=mid-label wildcard is rejected", entry: "https://foo.*.com", allowed: false},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := t.Context()
|
|
p := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.WithValues(map[string]interface{}{
|
|
config.ViperKeyURLsAllowedReturnToDomains: []string{tc.entry},
|
|
}), configx.SkipValidation())
|
|
|
|
domains := p.SelfServiceBrowserAllowedReturnToDomains(ctx)
|
|
if tc.allowed {
|
|
require.Len(t, domains, 1)
|
|
assert.Equal(t, tc.entry, domains[0].String())
|
|
} else {
|
|
assert.Empty(t, domains)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSelfServiceBrowserAllowedReturnToDomainsLegacyInsecure(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := t.Context()
|
|
|
|
// With legacy_allow_insecure_origins=true, unbounded wildcards that would
|
|
// otherwise be rejected must be kept.
|
|
p := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.WithValues(map[string]interface{}{
|
|
config.ViperKeyURLsAllowedReturnToDomains: []string{"https://*.vercel.app"},
|
|
config.ViperKeyLegacyAllowInsecureOrigins: true,
|
|
}), configx.SkipValidation())
|
|
|
|
domains := p.SelfServiceBrowserAllowedReturnToDomains(ctx)
|
|
require.Len(t, domains, 1, "unbounded wildcard must be kept when legacy_allow_insecure_origins is true")
|
|
assert.Equal(t, "https://*.vercel.app", domains[0].String())
|
|
}
|
|
|
|
const (
|
|
keyPublicTLSCertBase64 = "serve.public.tls.cert.base64"
|
|
keyPublicTLSKeyBase64 = "serve.public.tls.key.base64"
|
|
keyPublicTLSCertPath = "serve.public.tls.cert.path"
|
|
keyPublicTLSKeyPath = "serve.public.tls.key.path"
|
|
keyAdminTLSCertBase64 = "serve.admin.tls.cert.base64"
|
|
keyAdminTLSKeyBase64 = "serve.admin.tls.key.base64"
|
|
keyAdminTLSCertPath = "serve.admin.tls.cert.path"
|
|
keyAdminTLSKeyPath = "serve.admin.tls.key.path"
|
|
)
|