snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
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

This commit is contained in:
upstream-mirror
2026-09-15 21:19:12 +05:00
commit c60cc7ca75
8092 changed files with 394160 additions and 0 deletions

View File

@@ -0,0 +1,433 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package code_test
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
hydraclientgo "github.com/ory/hydra-client-go/v2"
"github.com/ory/x/configx"
"github.com/ory/kratos/courier"
"github.com/ory/kratos/courier/template"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/hydra"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/pkg"
"github.com/ory/kratos/pkg/testhelpers"
"github.com/ory/kratos/selfservice/flow"
"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/verification"
"github.com/ory/kratos/selfservice/strategy/code"
"github.com/ory/kratos/x"
"github.com/ory/kratos/x/nosurfx"
"github.com/ory/x/sqlxx"
"github.com/ory/x/urlx"
)
var b64 = func(str string) string {
return base64.StdEncoding.EncodeToString([]byte(str))
}
func TestSender(t *testing.T) {
t.Parallel()
ctx := context.Background()
conf, reg := pkg.NewFastRegistryWithMocks(t,
configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/default.schema.json")),
configx.WithValues(map[string]any{
config.ViperKeyPublicBaseURL: "https://www.ory.com/",
config.ViperKeyCourierSMTPURL: "smtp://foo@bar@dev.null/",
config.ViperKeySelfServiceRecoveryNotifyUnknownRecipients: true,
config.ViperKeySelfServiceVerificationNotifyUnknownRecipients: true,
}),
)
conf.MustSet(ctx, config.ViperKeyWebhookHeaderAllowlist, []string{"user-agent", "X-CUSTOM-HEADER"})
u := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.com/")}
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
// Fake phone numbers which are unallocated numbers that telephone companies cannot assign
// to customers in Germany under current legislation.
// This is to protect residents against the potential influx of phone calls
// that they may receive should their telephone numbers appear in a movie or film.
phoneNumberKnown := x.NormalizePhoneIdentifier("+49-160-555-5762")
phoneNumberUnknown := x.NormalizePhoneIdentifier("+49-155-555-4570")
i.Traits = identity.Traits(fmt.Sprintf(`{"email": "tracked@ory.sh", "phone": "%s"}`, phoneNumberKnown))
require.NoError(t, reg.IdentityManager().Create(ctx, i))
t.Run("method=SendRecoveryCode email", func(t *testing.T) {
recoveryCode := func(t *testing.T) {
t.Helper()
f, err := recovery.NewFlow(reg, time.Hour, "", u, recovery.Strategies{code.NewStrategy(reg)}, flow.TypeBrowser)
require.NoError(t, err)
require.NoError(t, reg.RecoveryFlowPersister().CreateRecoveryFlow(ctx, f))
header := http.Header{}
header.Add("user-agent", "user-agent header")
header.Add("X-CUSTOM-HEADER", "x-custom header 1")
header.Add("X-Custom-header", "x-custom header 2")
header.Add("some-other-header", "some-other-value")
require.NoError(t, reg.CodeSender().SendRecoveryCode(ctx, f, "email", "tracked@ory.sh", header))
require.ErrorIs(t, reg.CodeSender().SendRecoveryCode(ctx, f, "email", "not-tracked@ory.sh", header), code.ErrUnknownAddress())
}
t.Run("case=with default templates", func(t *testing.T) {
recoveryCode(t)
messages, err := reg.CourierPersister().NextMessages(ctx, 12)
require.NoError(t, err)
require.Len(t, messages, 2)
assert.EqualValues(t, "tracked@ory.sh", messages[0].Recipient)
assert.Contains(t, messages[0].Subject, "Use code")
assert.Regexp(t, testhelpers.CodeRegex, messages[0].Body)
assert.EqualValues(t, "not-tracked@ory.sh", messages[1].Recipient)
assert.Contains(t, messages[1].Subject, "Account access attempted")
assert.NotRegexp(t, testhelpers.CodeRegex, messages[1].Body, "Expected message to not contain an 6 digit recovery code, but it did: ", messages[1].Body)
headers := messages[0].RequestHeaders
assert.Equal(t, []any{"user-agent header"}, gjson.GetBytes(headers, "User-Agent").Value())
assert.Equal(t, []any{"x-custom header 1", "x-custom header 2"}, gjson.GetBytes(headers, "X-Custom-Header").Value())
assert.Empty(t, gjson.GetBytes(headers, "Some-Other-Header").Value(), "Expected Some-Other-Header to be empty")
})
t.Run("case=with custom templates", func(t *testing.T) {
subject := "custom template recovery code"
body := "custom template recovery code body"
t.Cleanup(func() {
conf.MustSet(ctx, config.ViperKeyCourierTemplatesRecoveryCodeInvalidEmail, nil)
conf.MustSet(ctx, config.ViperKeyCourierTemplatesRecoveryCodeValidEmail, nil)
})
conf.MustSet(ctx, config.ViperKeyCourierTemplatesRecoveryCodeInvalidEmail, fmt.Sprintf(`{ "subject": "base64://%s", "body": { "plaintext": "base64://%s", "html": "base64://%s" }}`, b64(subject+" invalid"), b64(body), b64(body)))
conf.MustSet(ctx, config.ViperKeyCourierTemplatesRecoveryCodeValidEmail, fmt.Sprintf(`{ "subject": "base64://%s", "body": { "plaintext": "base64://%s", "html": "base64://%s" }}`, b64(subject+" valid"), b64(body+" {{ .RecoveryCode }}"), b64(body+" {{ .RecoveryCode }}")))
recoveryCode(t)
messages, err := reg.CourierPersister().NextMessages(ctx, 12)
require.NoError(t, err)
require.Len(t, messages, 2)
assert.EqualValues(t, "tracked@ory.sh", messages[0].Recipient)
assert.Equal(t, messages[0].Subject, subject+" valid")
assert.Contains(t, messages[0].Body, body)
assert.Regexp(t, testhelpers.CodeRegex, messages[0].Body)
assert.EqualValues(t, "not-tracked@ory.sh", messages[1].Recipient)
assert.Equal(t, messages[1].Subject, subject+" invalid")
assert.Equal(t, messages[1].Body, body)
headers := messages[0].RequestHeaders
assert.Equal(t, []any{"user-agent header"}, gjson.GetBytes(headers, "User-Agent").Value())
assert.Equal(t, []any{"x-custom header 1", "x-custom header 2"}, gjson.GetBytes(headers, "X-Custom-Header").Value())
assert.Empty(t, gjson.GetBytes(headers, "Some-Other-Header").Value(), "Expected Some-Other-Header to be empty")
})
})
t.Run("method=SendRecoveryCode sms", func(t *testing.T) {
recoveryCode := func(t *testing.T) {
t.Helper()
f, err := recovery.NewFlow(reg, time.Hour, "", u, recovery.Strategies{code.NewStrategy(reg)}, flow.TypeBrowser)
require.NoError(t, err)
require.NoError(t, reg.RecoveryFlowPersister().CreateRecoveryFlow(ctx, f))
header := http.Header{}
header.Add("user-agent", "user-agent header")
header.Add("X-CUSTOM-HEADER", "x-custom header 1")
header.Add("X-Custom-header", "x-custom header 2")
header.Add("some-other-header", "some-other-value")
require.NoError(t, reg.CodeSender().SendRecoveryCode(ctx, f, "sms", phoneNumberKnown, header))
require.ErrorIs(t, reg.CodeSender().SendRecoveryCode(ctx, f, "sms", phoneNumberUnknown, header), code.ErrUnknownAddress())
}
t.Run("case=with default templates", func(t *testing.T) {
recoveryCode(t)
messages, err := reg.CourierPersister().NextMessages(ctx, 12)
require.NoError(t, err)
require.Len(t, messages, 1)
assert.EqualValues(t, phoneNumberKnown, messages[0].Recipient)
assert.Contains(t, messages[0].Body, "Your recovery code is:")
assert.Contains(t, messages[0].Body, "@www.ory.com #")
assert.Regexp(t, testhelpers.CodeRegex, messages[0].Body)
})
t.Run("case=with custom templates", func(t *testing.T) {
body := "custom template recovery code body"
t.Cleanup(func() {
conf.MustSet(ctx, config.ViperKeyCourierTemplatesRecoveryCodeValidSMS, nil)
})
conf.MustSet(ctx, config.ViperKeyCourierTemplatesRecoveryCodeValidSMS, fmt.Sprintf(`{ "body": { "plaintext": "base64://%s"}}`, b64(body+" {{ .RecoveryCode }}")))
recoveryCode(t)
messages, err := reg.CourierPersister().NextMessages(ctx, 12)
require.NoError(t, err)
require.Len(t, messages, 1)
assert.EqualValues(t, phoneNumberKnown, messages[0].Recipient)
assert.Contains(t, messages[0].Body, body)
headers := messages[0].RequestHeaders
assert.Equal(t, []any{"user-agent header"}, gjson.GetBytes(headers, "User-Agent").Value())
assert.Equal(t, []any{"x-custom header 1", "x-custom header 2"}, gjson.GetBytes(headers, "X-Custom-Header").Value())
assert.Empty(t, gjson.GetBytes(headers, "Some-Other-Header").Value(), "Expected Some-Other-Header to be empty")
assert.Regexp(t, testhelpers.CodeRegex, messages[0].Body)
})
})
t.Run("method=SendVerificationCode", func(t *testing.T) {
verificationFlow := func(t *testing.T) {
t.Helper()
f, err := verification.NewFlow(reg, time.Hour, "", u, verification.Strategies{code.NewStrategy(reg)}, flow.TypeBrowser)
require.NoError(t, err)
require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f))
require.NoError(t, reg.CodeSender().SendVerificationCode(ctx, f, "email", "tracked@ory.sh"))
require.ErrorIs(t, reg.CodeSender().SendVerificationCode(ctx, f, "email", "not-tracked@ory.sh"), code.ErrUnknownAddress())
}
t.Run("case=with default templates", func(t *testing.T) {
verificationFlow(t)
messages, err := reg.CourierPersister().NextMessages(ctx, 12)
require.NoError(t, err)
require.Len(t, messages, 2)
assert.EqualValues(t, "tracked@ory.sh", messages[0].Recipient)
assert.Contains(t, messages[0].Subject, "Use code")
assert.Regexp(t, testhelpers.CodeRegex, messages[0].Body)
assert.EqualValues(t, "not-tracked@ory.sh", messages[1].Recipient)
assert.Contains(t, messages[1].Subject, "Someone tried to verify this email address")
assert.NotRegexp(t, testhelpers.CodeRegex, messages[1].Body, "Expected message to not contain an 6 digit recovery code, but it did: ", messages[1].Body)
})
t.Run("case=with custom templates", func(t *testing.T) {
subject := "custom template verification code"
body := "custom template verification code body"
t.Cleanup(func() {
conf.MustSet(ctx, config.ViperKeyCourierTemplatesVerificationCodeInvalidEmail, nil)
conf.MustSet(ctx, config.ViperKeyCourierTemplatesVerificationCodeValidEmail, nil)
})
conf.MustSet(ctx, config.ViperKeyCourierTemplatesVerificationCodeInvalidEmail, fmt.Sprintf(`{ "subject": "base64://%s", "body": { "plaintext": "base64://%s", "html": "base64://%s" }}`, b64(subject+" invalid"), b64(body), b64(body)))
conf.MustSet(ctx, config.ViperKeyCourierTemplatesVerificationCodeValidEmail, fmt.Sprintf(`{ "subject": "base64://%s", "body": { "plaintext": "base64://%s", "html": "base64://%s" }}`, b64(subject+" valid"), b64(body+" {{ .VerificationCode }}"), b64(body+" {{ .VerificationCode }}")))
verificationFlow(t)
messages, err := reg.CourierPersister().NextMessages(ctx, 12)
require.NoError(t, err)
require.Len(t, messages, 2)
assert.EqualValues(t, "tracked@ory.sh", messages[0].Recipient)
assert.Equal(t, messages[0].Subject, subject+" valid")
assert.Contains(t, messages[0].Body, body)
assert.Regexp(t, testhelpers.CodeRegex, messages[0].Body)
assert.EqualValues(t, "not-tracked@ory.sh", messages[1].Recipient)
assert.Equal(t, messages[1].Subject, subject+" invalid")
assert.Equal(t, messages[1].Body, body)
})
})
t.Run("case=should be able to disable invalid email dispatch", func(t *testing.T) {
for _, tc := range []struct {
flow string
send func(t *testing.T)
configKey string
}{
{
flow: "recovery",
configKey: config.ViperKeySelfServiceRecoveryNotifyUnknownRecipients,
send: func(t *testing.T) {
s, _, err := reg.RecoveryStrategies(ctx).ActiveStrategies("code")
require.NoError(t, err)
f, err := recovery.NewFlow(reg, time.Hour, "", u, s, flow.TypeBrowser)
require.NoError(t, err)
require.NoError(t, reg.RecoveryFlowPersister().CreateRecoveryFlow(ctx, f))
err = reg.CodeSender().SendRecoveryCode(ctx, f, "email", "not-tracked@ory.sh", nil)
require.ErrorIs(t, err, code.ErrUnknownAddress())
},
},
{
flow: "verification",
configKey: config.ViperKeySelfServiceVerificationNotifyUnknownRecipients,
send: func(t *testing.T) {
s, _, err := reg.VerificationStrategies(ctx).ActiveStrategies("code")
require.NoError(t, err)
f, err := verification.NewFlow(reg, time.Hour, "", u, s, flow.TypeBrowser)
require.NoError(t, err)
require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f))
err = reg.CodeSender().SendVerificationCode(ctx, f, "email", "not-tracked@ory.sh")
require.ErrorIs(t, err, code.ErrUnknownAddress())
},
},
} {
t.Run("strategy="+tc.flow, func(t *testing.T) {
conf.MustSet(ctx, tc.configKey, false)
t.Cleanup(func() {
conf.MustSet(ctx, tc.configKey, true)
})
tc.send(t)
messages, err := reg.CourierPersister().NextMessages(ctx, 0)
require.ErrorIs(t, err, courier.ErrQueueEmpty)
require.Len(t, messages, 0)
})
}
})
t.Run("method=SendCode with OAuth2 login request", func(t *testing.T) {
reg.WithCSRFTokenGenerator(nosurfx.FakeCSRFTokenGenerator)
// Drain messages queued by earlier subtests.
_, _ = reg.CourierPersister().NextMessages(ctx, 100)
hlr := &hydraclientgo.OAuth2LoginRequest{
Challenge: "test-challenge",
Client: hydraclientgo.OAuth2Client{
ClientId: hydraclientgo.PtrString("test-client-id"),
ClientName: hydraclientgo.PtrString("Test Client"),
ClientUri: hydraclientgo.PtrString("https://client.example.com"),
LogoUri: hydraclientgo.PtrString("https://client.example.com/logo.png"),
Metadata: map[string]any{"brand": "acme"},
},
}
expectScopedRequest := func(t *testing.T, templateData []byte) {
t.Helper()
assert.Equal(t, "test-challenge", gjson.GetBytes(templateData, "oauth2_login_request.challenge").String())
assert.Equal(t, "test-client-id", gjson.GetBytes(templateData, "oauth2_login_request.client.client_id").String())
assert.Equal(t, "Test Client", gjson.GetBytes(templateData, "oauth2_login_request.client.client_name").String())
assert.Equal(t, "https://client.example.com", gjson.GetBytes(templateData, "oauth2_login_request.client.client_uri").String())
assert.Equal(t, "https://client.example.com/logo.png", gjson.GetBytes(templateData, "oauth2_login_request.client.logo_uri").String())
assert.Equal(t, "acme", gjson.GetBytes(templateData, "oauth2_login_request.client.metadata.brand").String())
// Only the scoped fields may be exposed to messaging channels.
assert.ElementsMatch(t, []string{"challenge", "client"}, jsonKeys(templateData, "oauth2_login_request"))
assert.ElementsMatch(t, []string{"client_id", "client_name", "client_uri", "logo_uri", "metadata"}, jsonKeys(templateData, "oauth2_login_request.client"))
}
t.Run("flow=login", func(t *testing.T) {
r := httptest.NewRequest("GET", "https://www.ory.com/self-service/login/browser", nil)
f, err := login.NewFlow(reg, r, flow.TypeBrowser)
require.NoError(t, err)
f.HydraLoginRequest = hlr
require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(ctx, f))
require.NoError(t, reg.CodeSender().SendCode(ctx, f, i, http.Header{},
code.Address{To: "tracked@ory.sh", Via: identity.ChannelTypeEmail},
code.Address{To: phoneNumberKnown, Via: identity.ChannelTypeSMS},
))
messages, err := reg.CourierPersister().NextMessages(ctx, 12)
require.NoError(t, err)
require.Len(t, messages, 2)
for _, m := range messages {
assert.EqualValues(t, template.TypeLoginCodeValid, m.TemplateType)
expectScopedRequest(t, m.TemplateData)
}
})
t.Run("flow=registration", func(t *testing.T) {
r := httptest.NewRequest("GET", "https://www.ory.com/self-service/registration/browser", nil)
f, err := registration.NewFlow(reg, r, flow.TypeBrowser)
require.NoError(t, err)
f.HydraLoginRequest = hlr
require.NoError(t, reg.RegistrationFlowPersister().CreateRegistrationFlow(ctx, f))
require.NoError(t, reg.CodeSender().SendCode(ctx, f, i, http.Header{},
code.Address{To: "tracked@ory.sh", Via: identity.ChannelTypeEmail},
code.Address{To: phoneNumberKnown, Via: identity.ChannelTypeSMS},
))
messages, err := reg.CourierPersister().NextMessages(ctx, 12)
require.NoError(t, err)
require.Len(t, messages, 2)
for _, m := range messages {
assert.EqualValues(t, template.TypeRegistrationCodeValid, m.TemplateType)
expectScopedRequest(t, m.TemplateData)
}
})
t.Run("flow=login without OAuth2 login request", func(t *testing.T) {
r := httptest.NewRequest("GET", "https://www.ory.com/self-service/login/browser", nil)
f, err := login.NewFlow(reg, r, flow.TypeBrowser)
require.NoError(t, err)
require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(ctx, f))
require.NoError(t, reg.CodeSender().SendCode(ctx, f, i, http.Header{},
code.Address{To: "tracked@ory.sh", Via: identity.ChannelTypeEmail},
))
messages, err := reg.CourierPersister().NextMessages(ctx, 12)
require.NoError(t, err)
require.Len(t, messages, 1)
assert.False(t, gjson.GetBytes(messages[0].TemplateData, "oauth2_login_request").Exists())
})
t.Run("flow=verification", func(t *testing.T) {
reg.SetHydra(hydra.NewFake())
f, err := verification.NewFlow(reg, time.Hour, "", u, verification.Strategies{code.NewStrategy(reg)}, flow.TypeBrowser)
require.NoError(t, err)
f.OAuth2LoginChallenge = sqlxx.NullString(hydra.FakeValidLoginChallenge)
require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f))
require.NoError(t, reg.CodeSender().SendVerificationCode(ctx, f, "email", "tracked@ory.sh"))
messages, err := reg.CourierPersister().NextMessages(ctx, 12)
require.NoError(t, err)
require.Len(t, messages, 1)
assert.EqualValues(t, template.TypeVerificationCodeValid, messages[0].TemplateType)
templateData := messages[0].TemplateData
assert.Equal(t, hydra.FakeValidLoginChallenge, gjson.GetBytes(templateData, "oauth2_login_request.challenge").String())
assert.Equal(t, hydra.FakeClientID, gjson.GetBytes(templateData, "oauth2_login_request.client.client_id").String())
assert.ElementsMatch(t, []string{"challenge", "client"}, jsonKeys(templateData, "oauth2_login_request"))
})
t.Run("flow=verification without OAuth2 login challenge", func(t *testing.T) {
f, err := verification.NewFlow(reg, time.Hour, "", u, verification.Strategies{code.NewStrategy(reg)}, flow.TypeBrowser)
require.NoError(t, err)
require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f))
require.NoError(t, reg.CodeSender().SendVerificationCode(ctx, f, "email", "tracked@ory.sh"))
messages, err := reg.CourierPersister().NextMessages(ctx, 12)
require.NoError(t, err)
require.Len(t, messages, 1)
assert.False(t, gjson.GetBytes(messages[0].TemplateData, "oauth2_login_request").Exists())
})
})
}
func jsonKeys(data []byte, path string) []string {
var keys []string
for key := range gjson.GetBytes(data, path).Map() {
keys = append(keys, key)
}
return keys
}