Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: delegate SMS code generation, sending and verification to an external service (PS-221) #4113

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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
2 changes: 2 additions & 0 deletions cmd/clidoc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ func init() {
"NewErrorValidationAddressUnknown": text.NewErrorValidationAddressUnknown(),
"NewInfoSelfServiceLoginCodeMFA": text.NewInfoSelfServiceLoginCodeMFA(),
"NewInfoSelfServiceLoginCodeMFAHint": text.NewInfoSelfServiceLoginCodeMFAHint("{maskedIdentifier}"),
"NewInfoNodeInputPhone": text.NewInfoNodeInputPhone(),
"NewInfoSelfServicePhoneVerificationSuccessful": text.NewInfoSelfServicePhoneVerificationSuccessful(),
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
You requested a verification code, but we couldn’t find an account linked to this email address.
Try a different way to log in, or create an account if you don’t have one already.
If you didn’t request a code, you can ignore this message.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
You requested a verification code, but we couldn’t find an account linked to this email address.
Try a different way to log in, or create an account if you don’t have one already.
If you didn’t request a code, you can ignore this message.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Someone tried to login using this email address
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
You requested a verification code, but we couldn’t find an account linked to this phone number. Try a different way to log in, or create an account if you don’t have one already. If you didn’t request a code, you can ignore this message.
54 changes: 54 additions & 0 deletions courier/template/email/login_code_invalid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package email

import (
"context"
"encoding/json"
"github.com/ory/kratos/courier/template"
"os"
"strings"
)

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

func NewLoginCodeInvalid(d template.Dependencies, m *LoginCodeInvalidModel) *LoginCodeInvalid {
return &LoginCodeInvalid{d: d, m: m}
}

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

func (t *LoginCodeInvalid) EmailSubject(ctx context.Context) (string, error) {
subject, err := template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "login_code/invalid/email.subject.gotmpl", "login_code/invalid/email.subject*", t.m, t.d.CourierConfig().CourierTemplatesLoginCodeInvalid(ctx).Subject)

return strings.TrimSpace(subject), err
}

func (t *LoginCodeInvalid) EmailBody(ctx context.Context) (string, error) {
return template.LoadHTML(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "login_code/invalid/email.body.gotmpl", "login_code/invalid/email.body*", t.m, t.d.CourierConfig().CourierTemplatesLoginCodeInvalid(ctx).Body.HTML)
}

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

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

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

package email_test

import (
"context"
"github.com/ory/kratos/courier/template/email"
"github.com/ory/kratos/courier/template/testhelpers"
"testing"

"github.com/ory/kratos/internal"
)

func TestNewLoginCodeInvalid(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
_, reg := internal.NewFastRegistryWithMocks(t)

tpl := email.NewLoginCodeInvalid(reg, &email.LoginCodeInvalidModel{})

testhelpers.TestRendered(t, ctx, tpl)
}
52 changes: 52 additions & 0 deletions courier/template/sms/login_code_invalid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package sms

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

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

type (
LoginCodeInvalid struct {
deps template.Dependencies
model *LoginCodeInvalidModel
}
LoginCodeInvalidModel struct {
To string `json:"to"`
RequestURL string `json:"request_url"`
TransientPayload map[string]interface{} `json:"transient_payload"`
}
)

func NewLoginCodeInvalid(d template.Dependencies, m *LoginCodeInvalidModel) *LoginCodeInvalid {
return &LoginCodeInvalid{deps: d, model: m}
}

func (t *LoginCodeInvalid) PhoneNumber() (string, error) {
return t.model.To, nil
}

func (t *LoginCodeInvalid) SMSBody(ctx context.Context) (string, error) {
return template.LoadText(
ctx,
t.deps,
os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)),
"login_code/invalid/sms.body.gotmpl",
"login_code/invalid/sms.body*",
t.model,
t.deps.CourierConfig().CourierSMSTemplatesLoginCodeInvalid(ctx).Body.PlainText,
)
}

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

func (t *LoginCodeInvalid) TemplateType() template.TemplateType {
return template.TypeLoginCodeInvalid
}
34 changes: 34 additions & 0 deletions courier/template/sms/login_code_invalid_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package sms_test

import (
"context"
"github.com/ory/kratos/courier/template/testhelpers"
"testing"

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

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

func TestNewLoginCodeInvalid(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
_, reg := internal.NewFastRegistryWithMocks(t)

const (
expectedPhone = "+12345678901"
)

tpl := sms.NewLoginCodeInvalid(reg, &sms.LoginCodeInvalidModel{To: expectedPhone})

testhelpers.TestSMSRendered(t, ctx, tpl)

actualPhone, err := tpl.PhoneNumber()
require.NoError(t, err)
assert.Equal(t, expectedPhone, actualPhone)
}
54 changes: 54 additions & 0 deletions courier/template/sms/registration_code_valid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package sms

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

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

type (
RegistrationCodeValid struct {
deps template.Dependencies
model *RegistrationCodeValidModel
}
RegistrationCodeValidModel struct {
To string `json:"to"`
RegistrationCode string `json:"registration_code"`
Traits map[string]interface{} `json:"traits"`
RequestURL string `json:"request_url"`
TransientPayload map[string]interface{} `json:"transient_payload"`
}
)

func NewRegistrationCodeValid(d template.Dependencies, m *RegistrationCodeValidModel) *RegistrationCodeValid {
return &RegistrationCodeValid{deps: d, model: m}
}

func (t *RegistrationCodeValid) PhoneNumber() (string, error) {
return t.model.To, nil
}

func (t *RegistrationCodeValid) SMSBody(ctx context.Context) (string, error) {
return template.LoadText(
ctx,
t.deps,
os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)),
"registration_code/valid/sms.body.gotmpl",
"registration_code/valid/sms.body*",
t.model,
t.deps.CourierConfig().CourierSMSTemplatesLoginCodeValid(ctx).Body.PlainText,
)
}

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

func (t *RegistrationCodeValid) TemplateType() template.TemplateType {
return template.TypeLoginCodeValid
}
1 change: 1 addition & 0 deletions courier/template/sms/verification_code.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type (

VerificationCodeValidModel struct {
To string `json:"to"`
VerificationURL string `json:"verification_url"`
VerificationCode string `json:"verification_code"`
Identity map[string]interface{} `json:"identity"`
RequestURL string `json:"request_url"`
Expand Down
9 changes: 9 additions & 0 deletions courier/template/testhelpers/testhelpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ func SetupRemoteConfig(t *testing.T, ctx context.Context, plaintext string, html
return reg
}

func TestSMSRendered(t *testing.T, ctx context.Context, tpl interface {
SMSBody(context.Context) (string, error)
},
) {
rendered, err := tpl.SMSBody(ctx)
require.NoError(t, err)
assert.NotEmpty(t, rendered)
}

func TestRendered(t *testing.T, ctx context.Context, tpl interface {
EmailBody(context.Context) (string, error)
EmailSubject(context.Context) (string, error)
Expand Down
1 change: 1 addition & 0 deletions courier/template/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const (
TypeVerificationCodeInvalid TemplateType = "verification_code_invalid"
TypeVerificationCodeValid TemplateType = "verification_code_valid"
TypeTestStub TemplateType = "stub"
TypeLoginCodeInvalid TemplateType = "login_code_invalid"
TypeLoginCodeValid TemplateType = "login_code_valid"
TypeRegistrationCodeValid TemplateType = "registration_code_valid"
)
50 changes: 48 additions & 2 deletions driver/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,12 @@ const (
ViperKeyCourierTemplatesVerificationCodeInvalidEmail = "courier.templates.verification_code.invalid.email"
ViperKeyCourierTemplatesVerificationCodeValidEmail = "courier.templates.verification_code.valid.email"
ViperKeyCourierTemplatesVerificationCodeValidSMS = "courier.templates.verification_code.valid.sms"
ViperKeyCourierTemplatesLoginCodeInvalidSMS = "courier.templates.login_code.invalid.sms"
ViperKeyCourierTemplatesLoginCodeValidSMS = "courier.templates.login_code.valid.sms"
ViperKeyCourierTemplatesRegistrationCodeValidSMS = "courier.templates.registration_code.valid.sms"
ViperKeyCourierDeliveryStrategy = "courier.delivery_strategy"
ViperKeyCourierHTTPRequestConfig = "courier.http.request_config"
ViperKeyCourierTemplatesLoginCodeInvalidEmail = "courier.templates.login_code.invalid.email"
ViperKeyCourierTemplatesLoginCodeValidEmail = "courier.templates.login_code.valid.email"
ViperKeyCourierTemplatesRegistrationCodeValidEmail = "courier.templates.registration_code.valid.email"
ViperKeyCourierSMTP = "courier.smtp"
Expand Down Expand Up @@ -241,10 +244,18 @@ type (
Enabled bool `json:"enabled"`
Config json.RawMessage `json:"config"`
}
ExternalSMSVerify struct {
Enabled bool `json:"enabled"`
VerificationStartRequest json.RawMessage `json:"verification_start_request"`
VerificationCheckRequest json.RawMessage `json:"verification_check_request"`
}

SelfServiceStrategyCode struct {
*SelfServiceStrategy
PasswordlessEnabled bool `json:"passwordless_enabled"`
MFAEnabled bool `json:"mfa_enabled"`
PasswordlessEnabled bool `json:"passwordless_enabled"`
MFAEnabled bool `json:"mfa_enabled"`
ExternalSMSVerify *ExternalSMSVerify `json:"external_sms_verify"`
NotifyUnknownRecipients bool `json:"notify_unknown_recipients"`
}
Schema struct {
ID string `json:"id" koanf:"id"`
Expand Down Expand Up @@ -309,10 +320,13 @@ type (
CourierTemplatesRecoveryCodeValid(ctx context.Context) *CourierEmailTemplate
CourierTemplatesVerificationCodeInvalid(ctx context.Context) *CourierEmailTemplate
CourierTemplatesVerificationCodeValid(ctx context.Context) *CourierEmailTemplate
CourierTemplatesLoginCodeInvalid(ctx context.Context) *CourierEmailTemplate
CourierTemplatesLoginCodeValid(ctx context.Context) *CourierEmailTemplate
CourierTemplatesRegistrationCodeValid(ctx context.Context) *CourierEmailTemplate
CourierSMSTemplatesVerificationCodeValid(ctx context.Context) *CourierSMSTemplate
CourierSMSTemplatesLoginCodeInvalid(ctx context.Context) *CourierSMSTemplate
CourierSMSTemplatesLoginCodeValid(ctx context.Context) *CourierSMSTemplate
CourierSMSTemplatesRegistrationCodeValid(ctx context.Context) *CourierSMSTemplate
CourierMessageRetries(ctx context.Context) int
CourierWorkerPullCount(ctx context.Context) int
CourierWorkerPullWait(ctx context.Context) time.Duration
Expand Down Expand Up @@ -794,6 +808,8 @@ func (p *Config) SelfServiceStrategy(ctx context.Context, strategy string) *Self
func (p *Config) SelfServiceCodeStrategy(ctx context.Context) *SelfServiceStrategyCode {
pp := p.GetProvider(ctx)
config := json.RawMessage("{}")
verificationStartRequest := json.RawMessage("{}")
verificationCheckRequest := json.RawMessage("{}")
basePath := ViperKeySelfServiceStrategyConfig + ".code"

var err error
Expand All @@ -803,13 +819,31 @@ func (p *Config) SelfServiceCodeStrategy(ctx context.Context) *SelfServiceStrate
config = json.RawMessage("{}")
}

verificationStartRequest, err = json.Marshal(pp.GetF(basePath+".external_sms_verify.verification_start_request", verificationStartRequest))
if err != nil {
p.l.WithError(err).Warn("Unable to marshal self service strategy verification_start_request.")
verificationStartRequest = json.RawMessage("{}")
}

verificationCheckRequest, err = json.Marshal(pp.GetF(basePath+".external_sms_verify.verification_check_request", verificationCheckRequest))
if err != nil {
p.l.WithError(err).Warn("Unable to marshal self service strategy verification_check_request.")
verificationCheckRequest = json.RawMessage("{}")
}

return &SelfServiceStrategyCode{
SelfServiceStrategy: &SelfServiceStrategy{
Enabled: pp.BoolF(basePath+".enabled", true),
Config: config,
},
PasswordlessEnabled: pp.BoolF(basePath+".passwordless_enabled", false),
MFAEnabled: pp.BoolF(basePath+".mfa_enabled", false),
ExternalSMSVerify: &ExternalSMSVerify{
Enabled: pp.BoolF(basePath+".external_sms_verify.enabled", false),
VerificationStartRequest: verificationStartRequest,
VerificationCheckRequest: verificationCheckRequest,
},
NotifyUnknownRecipients: pp.BoolF(basePath+".notify_unknown_recipients", false),
}
}

Expand Down Expand Up @@ -1143,10 +1177,22 @@ func (p *Config) CourierSMSTemplatesVerificationCodeValid(ctx context.Context) *
return p.CourierSMSTemplatesHelper(ctx, ViperKeyCourierTemplatesVerificationCodeValidSMS)
}

func (p *Config) CourierSMSTemplatesLoginCodeInvalid(ctx context.Context) *CourierSMSTemplate {
return p.CourierSMSTemplatesHelper(ctx, ViperKeyCourierTemplatesLoginCodeInvalidSMS)
}

func (p *Config) CourierSMSTemplatesLoginCodeValid(ctx context.Context) *CourierSMSTemplate {
return p.CourierSMSTemplatesHelper(ctx, ViperKeyCourierTemplatesLoginCodeValidSMS)
}

func (p *Config) CourierSMSTemplatesRegistrationCodeValid(ctx context.Context) *CourierSMSTemplate {
return p.CourierSMSTemplatesHelper(ctx, ViperKeyCourierTemplatesRegistrationCodeValidSMS)
}

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

func (p *Config) CourierTemplatesLoginCodeValid(ctx context.Context) *CourierEmailTemplate {
return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesLoginCodeValidEmail)
}
Expand Down
Loading