Skip to content

Commit

Permalink
feat: passwordless SMS and expiry notice in code / link templates (#4104
Browse files Browse the repository at this point in the history
)

This feature allows Ory Kratos to use the SMS gateway for login and registration with code via SMS.

Additionally, the default email and sms templates have been updated. We now also expose `ExpiresInMinutes` / `expires_in_minutes` in the templates, making it easier to remind the user how long the code or link is valid for.

Closes #1570
Closes #3779
  • Loading branch information
aeneasr authored Oct 4, 2024
1 parent df2e1f0 commit 462cea9
Show file tree
Hide file tree
Showing 83 changed files with 1,042 additions and 326 deletions.
2 changes: 1 addition & 1 deletion cmd/clidoc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ func init() {
"NewInfoSelfServiceLoginContinue": text.NewInfoSelfServiceLoginContinue(),
"NewErrorValidationSuchNoWebAuthnUser": text.NewErrorValidationSuchNoWebAuthnUser(),
"NewRegistrationEmailWithCodeSent": text.NewRegistrationEmailWithCodeSent(),
"NewLoginEmailWithCodeSent": text.NewLoginEmailWithCodeSent(),
"NewLoginCodeSent": text.NewLoginCodeSent(),
"NewErrorValidationRegistrationCodeInvalidOrAlreadyUsed": text.NewErrorValidationRegistrationCodeInvalidOrAlreadyUsed(),
"NewErrorValidationLoginCodeInvalidOrAlreadyUsed": text.NewErrorValidationLoginCodeInvalidOrAlreadyUsed(),
"NewErrorValidationNoCodeUser": text.NewErrorValidationNoCodeUser(),
Expand Down
36 changes: 8 additions & 28 deletions courier/courier.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,42 +47,22 @@ type (
}

courier struct {
courierChannels map[string]Channel
deps Dependencies
failOnDispatchError bool
backoff backoff.BackOff
deps Dependencies
failOnDispatchError bool
backoff backoff.BackOff
newEmailTemplateFromMessage func(d template.Dependencies, msg Message) (EmailTemplate, error)
}
)

func NewCourier(ctx context.Context, deps Dependencies) (Courier, error) {
return NewCourierWithCustomTemplates(ctx, deps, NewEmailTemplateFromMessage)
}

func NewCourierWithCustomTemplates(ctx context.Context, deps Dependencies, newEmailTemplateFromMessage func(d template.Dependencies, msg Message) (EmailTemplate, error)) (Courier, error) {
cs, err := deps.CourierConfig().CourierChannels(ctx)
if err != nil {
return nil, err
}
channels := make(map[string]Channel, len(cs))
for _, c := range cs {
switch c.Type {
case "smtp":
ch, err := NewSMTPChannelWithCustomTemplates(deps, c.SMTPConfig, newEmailTemplateFromMessage)
if err != nil {
return nil, err
}
channels[ch.ID()] = ch
case "http":
channels[c.ID] = newHttpChannel(c.ID, c.RequestConfig, deps)
default:
return nil, errors.Errorf("unknown courier channel type: %s", c.Type)
}
}

func NewCourierWithCustomTemplates(_ context.Context, deps Dependencies, newEmailTemplateFromMessage func(d template.Dependencies, msg Message) (EmailTemplate, error)) (Courier, error) {
return &courier{
deps: deps,
backoff: backoff.NewExponentialBackOff(),
courierChannels: channels,
deps: deps,
backoff: backoff.NewExponentialBackOff(),
newEmailTemplateFromMessage: newEmailTemplateFromMessage,
}, nil
}

Expand Down
36 changes: 33 additions & 3 deletions courier/courier_dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,33 @@ import (
"github.com/pkg/errors"
)

func (c *courier) channels(ctx context.Context, id string) (Channel, error) {
cs, err := c.deps.CourierConfig().CourierChannels(ctx)
if err != nil {
return nil, err
}

for _, channel := range cs {
if channel.ID != id {
continue
}
switch channel.Type {
case "smtp":
courierChannel, err := NewSMTPChannelWithCustomTemplates(c.deps, channel.SMTPConfig, c.newEmailTemplateFromMessage)
if err != nil {
return nil, err
}
return courierChannel, nil
case "http":
return newHttpChannel(channel.ID, channel.RequestConfig, c.deps), nil
default:
return nil, errors.Errorf("unknown courier channel type: %s", channel.Type)
}
}

return nil, errors.Errorf("no courier channels configured")
}

func (c *courier) DispatchMessage(ctx context.Context, msg Message) error {
logger := c.deps.Logger().
WithField("message_id", msg.ID).
Expand All @@ -24,9 +51,9 @@ func (c *courier) DispatchMessage(ctx context.Context, msg Message) error {
return err
}

channel, ok := c.courierChannels[msg.Channel.String()]
if !ok {
return errors.Errorf("message %s has unknown channel %q", msg.ID.String(), msg.Channel)
channel, err := c.channels(ctx, msg.Channel.String())
if err != nil {
return err
}

logger = logger.
Expand Down Expand Up @@ -80,6 +107,9 @@ func (c *courier) DispatchQueue(ctx context.Context) error {
logger.
Warnf(`Message was abandoned because it did not deliver after %d attempts`, msg.SendCount)
} else if err := c.DispatchMessage(ctx, msg); err != nil {
logger.
WithError(err).
Warn(`Unable to dispatch message.`)
if err := c.deps.CourierPersister().RecordDispatch(ctx, msg.ID, CourierMessageDispatchStatusFailed, err); err != nil {
logger.
WithError(err).
Expand Down
6 changes: 6 additions & 0 deletions courier/sms_templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ func NewSMSTemplateFromMessage(d template.Dependencies, m Message) (SMSTemplate,
return nil, err
}
return sms.NewLoginCodeValid(d, &t), nil
case template.TypeRegistrationCodeValid:
var t sms.RegistrationCodeValidModel
if err := json.Unmarshal(m.TemplateData, &t); err != nil {
return nil, err
}
return sms.NewRegistrationCodeValid(d, &t), nil

default:
return nil, errors.Errorf("received unexpected message template type: %s", m.TemplateType)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Hi,

please login to your account by entering the following code:
Login to your account with the following code:

{{ .LoginCode }}

It expires in {{ .ExpiresInMinutes }} minutes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Hi,

please login to your account by entering the following code:
Login to your account with the following code:

{{ .LoginCode }}

It expires in {{ .ExpiresInMinutes }} minutes.
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Login to your account
Use code {{ .LoginCode }} to log in
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
Your login code is: {{ .LoginCode }}

It expires in {{ .ExpiresInMinutes }} minutes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Hi,

please recover access to your account by clicking the following link:
Recover access to your account by clicking the following link:

<a href="{{ .RecoveryURL }}">{{ .RecoveryURL }}</a>

If this was not you, do nothing. This link expires in {{ .ExpiresInMinutes }} minutes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
Hi,

please recover access to your account by clicking the following link:
Recover access to your account by clicking the following link:

{{ .RecoveryURL }}

If this was not you, do nothing. This link expires in {{ .ExpiresInMinutes }} minutes.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Hi,

please recover access to your account by entering the following code:
Recover access to your account by entering the following code:

{{ .RecoveryCode }}

If this was not you, do nothing. This code expires in {{ .ExpiresInMinutes }} minutes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Hi,

please recover access to your account by entering the following code:
Recover access to your account by entering the following code:

{{ .RecoveryCode }}

If this was not you, do nothing. This code expires in {{ .ExpiresInMinutes }} minutes.
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Recover access to your account
Use code {{ .RecoveryCode }} to recover access to your account
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Hi,

please complete your account registration by entering the following code:
Complete your account registration with the following code:

{{ .RegistrationCode }}

This code expires in {{ .ExpiresInMinutes }} minutes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Hi,

please complete your account registration by entering the following code:
Complete your account registration with the following code:

{{ .RegistrationCode }}

This code expires in {{ .ExpiresInMinutes }} minutes.
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Complete your account registration
Use code {{ .RegistrationCode }} to complete your account registration
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Your registration code is: {{ .RegistrationCode }}

It expires in {{ .ExpiresInMinutes }} minutes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
Hi,

someone asked to verify this email address, but we were unable to find an account for this address.
Someone asked to verify this email address, but we were unable to find an account for this address.

If this was you, check if you signed up using a different address.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
Hi,

someone asked to verify this email address, but we were unable to find an account for this address.
Someone asked to verify this email address, but we were unable to find an account for this address.

If this was you, check if you signed up using a different address.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
Hi, please verify your account by clicking the following link:
Verify your account by opening the following link:

<a href="{{ .VerificationURL }}">{{ .VerificationURL }}</a>

If this was not you, do nothing. This link expires in {{ .ExpiresInMinutes }} minutes.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Hi, please verify your account by clicking the following link:
Verify your account by opening the following link:

{{ .VerificationURL }}

If this was not you, do nothing. This link expires in {{ .ExpiresInMinutes }} minutes.
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
Hi,

please verify your account by entering the following code:
Verify your account with the following code:

{{ .VerificationCode }}

or clicking the following link:

<a href="{{ .VerificationURL }}">{{ .VerificationURL }}</a>

If this was not you, do nothing. This code / link expires in {{ .ExpiresInMinutes }} minutes.
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
Hi,

please verify your account by entering the following code:
Verify your account with the following code:

{{ .VerificationCode }}

or clicking the following link:

{{ .VerificationURL }}

If this was not you, do nothing. This code / link expires in {{ .ExpiresInMinutes }} minutes.
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Please verify your email address
Use code {{ .VerificationCode }} to verify your account
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
Your verification code is: {{ .VerificationCode }}

If this was not you, do nothing. It expires in {{ .ExpiresInMinutes }} minutes.
1 change: 1 addition & 0 deletions courier/template/email/login_code_valid.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type (
Identity map[string]interface{} `json:"identity"`
RequestURL string `json:"request_url"`
TransientPayload map[string]interface{} `json:"transient_payload"`
ExpiresInMinutes int `json:"expires_in_minutes"`
}
)

Expand Down
1 change: 1 addition & 0 deletions courier/template/email/recovery_code_valid.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type (
Identity map[string]interface{} `json:"identity"`
RequestURL string `json:"request_url"`
TransientPayload map[string]interface{} `json:"transient_payload"`
ExpiresInMinutes int `json:"expires_in_minutes"`
}
)

Expand Down
1 change: 1 addition & 0 deletions courier/template/email/recovery_valid.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type (
Identity map[string]interface{} `json:"identity"`
RequestURL string `json:"request_url"`
TransientPayload map[string]interface{} `json:"transient_payload"`
ExpiresInMinutes int `json:"expires_in_minutes"`
}
)

Expand Down
1 change: 1 addition & 0 deletions courier/template/email/registration_code_valid.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type (
RegistrationCode string `json:"registration_code"`
RequestURL string `json:"request_url"`
TransientPayload map[string]interface{} `json:"transient_payload"`
ExpiresInMinutes int `json:"expires_in_minutes"`
}
)

Expand Down
1 change: 1 addition & 0 deletions courier/template/email/verification_code_valid.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type (
Identity map[string]interface{} `json:"identity"`
RequestURL string `json:"request_url"`
TransientPayload map[string]interface{} `json:"transient_payload"`
ExpiresInMinutes int `json:"expires_in_minutes"`
}
)

Expand Down
1 change: 1 addition & 0 deletions courier/template/email/verification_valid.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type (
Identity map[string]interface{} `json:"identity"`
RequestURL string `json:"request_url"`
TransientPayload map[string]interface{} `json:"transient_payload"`
ExpiresInMinutes int `json:"expires_in_minutes"`
}
)

Expand Down
1 change: 1 addition & 0 deletions courier/template/sms/login_code_valid.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type (
Identity map[string]interface{} `json:"identity"`
RequestURL string `json:"request_url"`
TransientPayload map[string]interface{} `json:"transient_payload"`
ExpiresInMinutes int `json:"expires_in_minutes"`
}
)

Expand Down
2 changes: 1 addition & 1 deletion courier/template/sms/login_code_valid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func TestNewLoginCodeValid(t *testing.T) {

tpl := sms.NewLoginCodeValid(reg, &sms.LoginCodeValidModel{To: expectedPhone, LoginCode: otp})

expectedBody := fmt.Sprintf("Your login code is: %s\n", otp)
expectedBody := fmt.Sprintf("Your login code is: %s\n\nIt expires in 0 minutes.\n", otp)

actualBody, err := tpl.SMSBody(context.Background())
require.NoError(t, err)
Expand Down
Loading

0 comments on commit 462cea9

Please sign in to comment.