Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .schemastore/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1011,7 +1011,19 @@
"$ref": "#/definitions/defaultReturnTo"
},
"hooks": {
"$ref": "#/definitions/selfServiceHooks"
"type": "array",
"items": {
"anyOf": [
{
"$ref": "#/definitions/selfServiceSessionIssuerHook"
},
{
"$ref": "#/definitions/selfServiceWebHook"
}
]
},
"uniqueItems": true,
"additionalItems": false
}
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Someone tried to create an account with this email address.

If this was you, you may already have an account. Try signing in instead.

If this was not you, you can safely ignore this email.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Someone tried to create an account with this email address.

If this was you, you may already have an account. Try signing in instead.

If this was not you, you can safely ignore this email.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Account registration attempt
56 changes: 56 additions & 0 deletions courier/template/email/registration_duplicate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright © 2025 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package email

import (
"context"
"encoding/json"
"os"
"strings"

"github.com/ory/kratos/courier/template"
)

type (
RegistrationDuplicate struct {
d template.Dependencies
m *RegistrationDuplicateModel
}
RegistrationDuplicateModel struct {
To string `json:"to"`
Identity map[string]interface{} `json:"identity"`
RequestURL string `json:"request_url"`
TransientPayload map[string]interface{} `json:"transient_payload"`
}
)

func NewRegistrationDuplicate(d template.Dependencies, m *RegistrationDuplicateModel) *RegistrationDuplicate {
return &RegistrationDuplicate{d: d, m: m}
}

func (t *RegistrationDuplicate) EmailRecipient() (string, error) {
return t.m.To, nil
}

func (t *RegistrationDuplicate) EmailSubject(ctx context.Context) (string, error) {
subject, err := template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "registration/duplicate/email.subject.gotmpl", "registration/duplicate/email.subject*", t.m, t.d.CourierConfig().CourierTemplatesRegistrationDuplicate(ctx).Subject)

return strings.TrimSpace(subject), err
}

func (t *RegistrationDuplicate) EmailBody(ctx context.Context) (string, error) {
return template.LoadHTML(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "registration/duplicate/email.body.gotmpl", "registration/duplicate/email.body*", t.m, t.d.CourierConfig().CourierTemplatesRegistrationDuplicate(ctx).Body.HTML)
}

func (t *RegistrationDuplicate) EmailBodyPlaintext(ctx context.Context) (string, error) {
return template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "registration/duplicate/email.body.plaintext.gotmpl", "registration/duplicate/email.body.plaintext*", t.m, t.d.CourierConfig().CourierTemplatesRegistrationDuplicate(ctx).Body.PlainText)
}

func (t *RegistrationDuplicate) MarshalJSON() ([]byte, error) {
return json.Marshal(t.m)
}

func (t *RegistrationDuplicate) TemplateType() template.TemplateType {
return template.TypeRegistrationDuplicateEmail
}
60 changes: 60 additions & 0 deletions courier/template/email/registration_duplicate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright © 2025 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package email_test

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/ory/kratos/courier/template/email"
"github.com/ory/kratos/internal"
)

func TestNewRegistrationDuplicate(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)

_, reg := internal.NewFastRegistryWithMocks(t)

model := &email.RegistrationDuplicateModel{
To: "[email protected]",
RequestURL: "https://www.ory.sh/verify",
TransientPayload: map[string]interface{}{
"foo": "bar",
},
}

tpl := email.NewRegistrationDuplicate(reg, model)

t.Run("case=renders subject", func(t *testing.T) {
subject, err := tpl.EmailSubject(ctx)
require.NoError(t, err)
assert.NotEmpty(t, subject)
assert.Contains(t, subject, "Account registration attempt")
})

t.Run("case=renders body html", func(t *testing.T) {
body, err := tpl.EmailBody(ctx)
require.NoError(t, err)
assert.NotEmpty(t, body)
assert.Contains(t, body, "Someone tried to create an account")
})

t.Run("case=renders body plaintext", func(t *testing.T) {
body, err := tpl.EmailBodyPlaintext(ctx)
require.NoError(t, err)
assert.NotEmpty(t, body)
assert.Contains(t, body, "Someone tried to create an account")
})

t.Run("case=email recipient", func(t *testing.T) {
recipient, err := tpl.EmailRecipient()
require.NoError(t, err)
assert.Equal(t, "[email protected]", recipient)
})
}
23 changes: 12 additions & 11 deletions courier/template/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ package template
type TemplateType string

const (
TypeRecoveryInvalid TemplateType = "recovery_invalid"
TypeRecoveryValid TemplateType = "recovery_valid"
TypeRecoveryCodeInvalid TemplateType = "recovery_code_invalid"
TypeRecoveryCodeValid TemplateType = "recovery_code_valid"
TypeVerificationInvalid TemplateType = "verification_invalid"
TypeVerificationValid TemplateType = "verification_valid"
TypeVerificationCodeInvalid TemplateType = "verification_code_invalid"
TypeVerificationCodeValid TemplateType = "verification_code_valid"
TypeTestStub TemplateType = "stub"
TypeLoginCodeValid TemplateType = "login_code_valid"
TypeRegistrationCodeValid TemplateType = "registration_code_valid"
TypeRecoveryInvalid TemplateType = "recovery_invalid"
TypeRecoveryValid TemplateType = "recovery_valid"
TypeRecoveryCodeInvalid TemplateType = "recovery_code_invalid"
TypeRecoveryCodeValid TemplateType = "recovery_code_valid"
TypeVerificationInvalid TemplateType = "verification_invalid"
TypeVerificationValid TemplateType = "verification_valid"
TypeVerificationCodeInvalid TemplateType = "verification_code_invalid"
TypeVerificationCodeValid TemplateType = "verification_code_valid"
TypeTestStub TemplateType = "stub"
TypeLoginCodeValid TemplateType = "login_code_valid"
TypeRegistrationCodeValid TemplateType = "registration_code_valid"
TypeRegistrationDuplicateEmail TemplateType = "registration_duplicate"
)
6 changes: 6 additions & 0 deletions courier/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ func NewEmailTemplateFromMessage(d template.Dependencies, msg Message) (EmailTem
return nil, err
}
return email.NewRegistrationCodeValid(d, &t), nil
case template.TypeRegistrationDuplicateEmail:
var t email.RegistrationDuplicateModel
if err := json.Unmarshal(msg.TemplateData, &t); err != nil {
return nil, err
}
return email.NewRegistrationDuplicate(d, &t), nil
default:
return nil, errors.Errorf("received unexpected message template type: %s", msg.TemplateType)
}
Expand Down
6 changes: 6 additions & 0 deletions driver/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ const (
ViperKeyCourierHTTPRequestConfig = "courier.http.request_config"
ViperKeyCourierTemplatesLoginCodeValidEmail = "courier.templates.login_code.valid.email"
ViperKeyCourierTemplatesRegistrationCodeValidEmail = "courier.templates.registration_code.valid.email"
ViperKeyCourierTemplatesRegistrationDuplicateEmail = "courier.templates.registration.duplicate.email"
ViperKeyCourierSMTP = "courier.smtp"
ViperKeyCourierSMTPFrom = "courier.smtp.from_address"
ViperKeyCourierSMTPFromName = "courier.smtp.from_name"
Expand Down Expand Up @@ -314,6 +315,7 @@ type (
CourierTemplatesVerificationCodeValid(ctx context.Context) *CourierEmailTemplate
CourierTemplatesLoginCodeValid(ctx context.Context) *CourierEmailTemplate
CourierTemplatesRegistrationCodeValid(ctx context.Context) *CourierEmailTemplate
CourierTemplatesRegistrationDuplicate(ctx context.Context) *CourierEmailTemplate
CourierSMSTemplatesVerificationCodeValid(ctx context.Context) *CourierSMSTemplate
CourierSMSTemplatesRecoveryCodeValid(ctx context.Context) *CourierSMSTemplate
CourierSMSTemplatesLoginCodeValid(ctx context.Context) *CourierSMSTemplate
Expand Down Expand Up @@ -1177,6 +1179,10 @@ func (p *Config) CourierSMSTemplatesRegistrationCodeValid(ctx context.Context) *
return p.CourierSMSTemplatesHelper(ctx, ViperKeyCourierTemplatesRegistrationCodeValidSMS)
}

func (p *Config) CourierTemplatesRegistrationDuplicate(ctx context.Context) *CourierEmailTemplate {
return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesRegistrationDuplicateEmail)
}

func (p *Config) CourierTemplatesLoginCodeValid(ctx context.Context) *CourierEmailTemplate {
return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesLoginCodeValidEmail)
}
Expand Down
1 change: 1 addition & 0 deletions driver/registry_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ type RegistryDefault struct {
selfserviceRegistrationHandler *registration.Handler
seflserviceRegistrationErrorHandler *registration.ErrorHandler
selfserviceRegistrationRequestErrorHandler *registration.ErrorHandler
selfserviceRegistrationSender *registration.Sender

selfserviceLoginExecutor *login.HookExecutor
selfserviceLoginHandler *login.Handler
Expand Down
25 changes: 24 additions & 1 deletion driver/registry_default_hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
package driver

import (
"context"
"encoding/json"
"fmt"

"github.com/pkg/errors"

"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/request"
"github.com/ory/kratos/selfservice/hook"
)
Expand Down Expand Up @@ -52,18 +54,39 @@ func (m *RegistryDefault) HookShowVerificationUI() *hook.ShowVerificationUIHook
func (m *RegistryDefault) WithHooks(hooks map[string]func(config.SelfServiceHook) interface{}) {
m.injectedSelfserviceHooks = hooks
}

func (m *RegistryDefault) WithExtraHandlers(handlers []NewHandlerRegistrar) {
m.extraHandlerFactories = handlers
}

func getHooks[T any](m *RegistryDefault, credentialsType string, configs []config.SelfServiceHook) ([]T, error) {
func isEnumerationSafeType(credentialsType string) bool {
switch credentialsType {
case identity.CredentialsTypeOIDC.String(),
identity.CredentialsTypeCodeAuth.String(),
config.HookGlobal:
return true
default:
return false
}
}

func getHooks[T any](m *RegistryDefault, ctx context.Context, credentialsType string, configs []config.SelfServiceHook) ([]T, error) {
hooks := make([]T, 0, len(configs))

var addSessionIssuer bool
allHooksLoop:
for _, h := range configs {
switch h.Name {
case hook.KeySessionIssuer:

if m.Config().SecurityAccountEnumerationMitigate(ctx) && !isEnumerationSafeType(credentialsType) {
m.l.WithField("for", credentialsType).Error("The 'session' hook is incompatible with account enumeration mitigation")
return nil, errors.Errorf(
"the 'session' hook for %s is incompatible with security.account_enumeration.mitigate=true: "+
"issuing sessions during anti-enumeration flows would leak information about existing accounts. "+
"Please remove the 'session' hook from selfservice.flows.registration.after.%s.hooks or disable account enumeration mitigation",
credentialsType, credentialsType)
}
// The session issuer hook always needs to come last.
addSessionIssuer = true
case hook.KeySessionDestroyer:
Expand Down
6 changes: 3 additions & 3 deletions driver/registry_default_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ func (m *RegistryDefault) LoginHookExecutor() *login.HookExecutor {
}

func (m *RegistryDefault) PreLoginHooks(ctx context.Context) ([]login.PreHookExecutor, error) {
return getHooks[login.PreHookExecutor](m, "", m.Config().SelfServiceFlowLoginBeforeHooks(ctx))
return getHooks[login.PreHookExecutor](m, ctx, "", m.Config().SelfServiceFlowLoginBeforeHooks(ctx))
}

func (m *RegistryDefault) PostLoginHooks(ctx context.Context, credentialsType identity.CredentialsType) ([]login.PostHookExecutor, error) {
hooks, err := getHooks[login.PostHookExecutor](m, string(credentialsType), m.Config().SelfServiceFlowLoginAfterHooks(ctx, string(credentialsType)))
hooks, err := getHooks[login.PostHookExecutor](m, ctx, string(credentialsType), m.Config().SelfServiceFlowLoginAfterHooks(ctx, string(credentialsType)))
if err != nil {
return nil, err
}
Expand All @@ -33,7 +33,7 @@ func (m *RegistryDefault) PostLoginHooks(ctx context.Context, credentialsType id

// since we don't want merging hooks defined in a specific strategy and global hooks
// global hooks are added only if no strategy specific hooks are defined
return getHooks[login.PostHookExecutor](m, config.HookGlobal, m.Config().SelfServiceFlowLoginAfterHooks(ctx, config.HookGlobal))
return getHooks[login.PostHookExecutor](m, ctx, config.HookGlobal, m.Config().SelfServiceFlowLoginAfterHooks(ctx, config.HookGlobal))
}

func (m *RegistryDefault) LoginHandler() *login.Handler {
Expand Down
4 changes: 2 additions & 2 deletions driver/registry_default_recovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@ func (m *RegistryDefault) RecoveryExecutor() *recovery.HookExecutor {
}

func (m *RegistryDefault) PreRecoveryHooks(ctx context.Context) ([]recovery.PreHookExecutor, error) {
return getHooks[recovery.PreHookExecutor](m, "", m.Config().SelfServiceFlowRecoveryBeforeHooks(ctx))
return getHooks[recovery.PreHookExecutor](m, ctx, "", m.Config().SelfServiceFlowRecoveryBeforeHooks(ctx))
}

func (m *RegistryDefault) PostRecoveryHooks(ctx context.Context) ([]recovery.PostHookExecutor, error) {
return getHooks[recovery.PostHookExecutor](m, config.HookGlobal, m.Config().SelfServiceFlowRecoveryAfterHooks(ctx, config.HookGlobal))
return getHooks[recovery.PostHookExecutor](m, ctx, config.HookGlobal, m.Config().SelfServiceFlowRecoveryAfterHooks(ctx, config.HookGlobal))
}

func (m *RegistryDefault) CodeSender() *code.Sender {
Expand Down
15 changes: 11 additions & 4 deletions driver/registry_default_registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
)

func (m *RegistryDefault) PostRegistrationPrePersistHooks(ctx context.Context, credentialsType identity.CredentialsType) ([]registration.PostHookPrePersistExecutor, error) {
hooks, err := getHooks[registration.PostHookPrePersistExecutor](m, string(credentialsType), m.Config().SelfServiceFlowRegistrationAfterHooks(ctx, string(credentialsType)))
hooks, err := getHooks[registration.PostHookPrePersistExecutor](m, ctx, string(credentialsType), m.Config().SelfServiceFlowRegistrationAfterHooks(ctx, string(credentialsType)))
if err != nil {
return nil, err
}
Expand All @@ -22,14 +22,14 @@ func (m *RegistryDefault) PostRegistrationPrePersistHooks(ctx context.Context, c
}

func (m *RegistryDefault) PostRegistrationPostPersistHooks(ctx context.Context, credentialsType identity.CredentialsType) ([]registration.PostHookPostPersistExecutor, error) {
hooks, err := getHooks[registration.PostHookPostPersistExecutor](m, string(credentialsType), m.Config().SelfServiceFlowRegistrationAfterHooks(ctx, string(credentialsType)))
hooks, err := getHooks[registration.PostHookPostPersistExecutor](m, ctx, string(credentialsType), m.Config().SelfServiceFlowRegistrationAfterHooks(ctx, string(credentialsType)))
if err != nil {
return nil, err
}
if len(hooks) == 0 {
// since we don't want merging hooks defined in a specific strategy and
// global hooks are added only if no strategy specific hooks are defined
hooks, err = getHooks[registration.PostHookPostPersistExecutor](m, config.HookGlobal, m.Config().SelfServiceFlowRegistrationAfterHooks(ctx, config.HookGlobal))
hooks, err = getHooks[registration.PostHookPostPersistExecutor](m, ctx, config.HookGlobal, m.Config().SelfServiceFlowRegistrationAfterHooks(ctx, config.HookGlobal))
if err != nil {
return nil, err
}
Expand All @@ -44,7 +44,7 @@ func (m *RegistryDefault) PostRegistrationPostPersistHooks(ctx context.Context,
}

func (m *RegistryDefault) PreRegistrationHooks(ctx context.Context) ([]registration.PreHookExecutor, error) {
return getHooks[registration.PreHookExecutor](m, "", m.Config().SelfServiceFlowRegistrationBeforeHooks(ctx))
return getHooks[registration.PreHookExecutor](m, ctx, "", m.Config().SelfServiceFlowRegistrationBeforeHooks(ctx))
}

func (m *RegistryDefault) RegistrationExecutor() *registration.HookExecutor {
Expand Down Expand Up @@ -83,3 +83,10 @@ func (m *RegistryDefault) RegistrationFlowErrorHandler() *registration.ErrorHand

return m.selfserviceRegistrationRequestErrorHandler
}

func (m *RegistryDefault) RegistrationSender() *registration.Sender {
if m.selfserviceRegistrationSender == nil {
m.selfserviceRegistrationSender = registration.NewSender(m)
}
return m.selfserviceRegistrationSender
}
8 changes: 4 additions & 4 deletions driver/registry_default_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,22 @@ import (
)

func (m *RegistryDefault) PostSettingsPrePersistHooks(ctx context.Context, settingsType string) ([]settings.PostHookPrePersistExecutor, error) {
return getHooks[settings.PostHookPrePersistExecutor](m, settingsType, m.Config().SelfServiceFlowSettingsAfterHooks(ctx, settingsType))
return getHooks[settings.PostHookPrePersistExecutor](m, ctx, settingsType, m.Config().SelfServiceFlowSettingsAfterHooks(ctx, settingsType))
}

func (m *RegistryDefault) PreSettingsHooks(ctx context.Context) ([]settings.PreHookExecutor, error) {
return getHooks[settings.PreHookExecutor](m, "", m.Config().SelfServiceFlowSettingsBeforeHooks(ctx))
return getHooks[settings.PreHookExecutor](m, ctx, "", m.Config().SelfServiceFlowSettingsBeforeHooks(ctx))
}

func (m *RegistryDefault) PostSettingsPostPersistHooks(ctx context.Context, settingsType string) ([]settings.PostHookPostPersistExecutor, error) {
hooks, err := getHooks[settings.PostHookPostPersistExecutor](m, settingsType, m.Config().SelfServiceFlowSettingsAfterHooks(ctx, settingsType))
hooks, err := getHooks[settings.PostHookPostPersistExecutor](m, ctx, settingsType, m.Config().SelfServiceFlowSettingsAfterHooks(ctx, settingsType))
if err != nil {
return nil, err
}
if len(hooks) == 0 {
// since we don't want merging hooks defined in a specific strategy and
// global hooks are added only if no strategy specific hooks are defined
hooks, err = getHooks[settings.PostHookPostPersistExecutor](m, config.HookGlobal, m.Config().SelfServiceFlowSettingsAfterHooks(ctx, config.HookGlobal))
hooks, err = getHooks[settings.PostHookPostPersistExecutor](m, ctx, config.HookGlobal, m.Config().SelfServiceFlowSettingsAfterHooks(ctx, config.HookGlobal))
if err != nil {
return nil, err
}
Expand Down
Loading
Loading