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

OTP recovery and verification strategy #2184

Closed
Closed
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 @@ -69,6 +69,7 @@ func init() {
"NewErrorValidationVerificationFlowExpired": text.NewErrorValidationVerificationFlowExpired(-time.Second),
"NewInfoSelfServiceVerificationSuccessful": text.NewInfoSelfServiceVerificationSuccessful(),
"NewVerificationEmailSent": text.NewVerificationEmailSent(),
"NewVerificationOTPSent": text.NewVerificationOTPSent(),
"NewErrorValidationVerificationTokenInvalidOrAlreadyUsed": text.NewErrorValidationVerificationTokenInvalidOrAlreadyUsed(),
"NewErrorValidationVerificationRetrySuccess": text.NewErrorValidationVerificationRetrySuccess(),
"NewErrorValidationVerificationStateFailure": text.NewErrorValidationVerificationStateFailure(),
Expand Down Expand Up @@ -111,6 +112,7 @@ func init() {
"NewErrorValidationRecoveryFlowExpired": text.NewErrorValidationRecoveryFlowExpired(time.Second),
"NewRecoverySuccessful": text.NewRecoverySuccessful(inAMinute),
"NewRecoveryEmailSent": text.NewRecoveryEmailSent(),
"NewRecoveryOTPSent": text.NewRecoveryOTPSent(),
"NewErrorValidationRecoveryTokenInvalidOrAlreadyUsed": text.NewErrorValidationRecoveryTokenInvalidOrAlreadyUsed(),
"NewErrorValidationRecoveryRetrySuccess": text.NewErrorValidationRecoveryRetrySuccess(),
"NewErrorValidationRecoveryStateFailure": text.NewErrorValidationRecoveryStateFailure(),
Expand Down
29 changes: 23 additions & 6 deletions courier/email_templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@ type (
)

const (
TypeRecoveryInvalid TemplateType = "recovery_invalid"
TypeRecoveryValid TemplateType = "recovery_valid"
TypeVerificationInvalid TemplateType = "verification_invalid"
TypeVerificationValid TemplateType = "verification_valid"
TypeOTP TemplateType = "otp"
TypeTestStub TemplateType = "stub"
TypeRecoveryInvalid TemplateType = "recovery_invalid"
TypeRecoveryValid TemplateType = "recovery_valid"
TypeRecoveryValidOTP TemplateType = "recovery_valid_otp"
TypeVerificationInvalid TemplateType = "verification_invalid"
TypeVerificationValid TemplateType = "verification_valid"
TypeVerificationValidOTP TemplateType = "verification_valid_otp"
TypeTestStub TemplateType = "stub"
)

func GetEmailTemplateType(t EmailTemplate) (TemplateType, error) {
Expand All @@ -42,6 +43,10 @@ func GetEmailTemplateType(t EmailTemplate) (TemplateType, error) {
return TypeVerificationInvalid, nil
case *email.VerificationValid:
return TypeVerificationValid, nil
case *email.RecoveryValidOTP:
return TypeRecoveryValidOTP, nil
case *email.VerificationValidOTP:
return TypeVerificationValidOTP, nil
case *email.TestStub:
return TypeTestStub, nil
default:
Expand Down Expand Up @@ -75,6 +80,18 @@ func NewEmailTemplateFromMessage(d template.Dependencies, msg Message) (EmailTem
return nil, err
}
return email.NewVerificationValid(d, &t), nil
case TypeRecoveryValidOTP:
var t email.RecoveryValidOTPModel
if err := json.Unmarshal(msg.TemplateData, &t); err != nil {
return nil, err
}
return email.NewRecoveryValidOTP(d, &t), nil
case TypeVerificationValidOTP:
var t email.VerificationValidOTPModel
if err := json.Unmarshal(msg.TemplateData, &t); err != nil {
return nil, err
}
return email.NewVerificationValidOTP(d, &t), nil
case TypeTestStub:
var t email.TestStubModel
if err := json.Unmarshal(msg.TemplateData, &t); err != nil {
Expand Down
18 changes: 13 additions & 5 deletions courier/sms_templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ type SMSTemplate interface {

func SMSTemplateType(t SMSTemplate) (TemplateType, error) {
switch t.(type) {
case *sms.OTPMessage:
return TypeOTP, nil
case *sms.RecoveryMessage:
return TypeRecoveryValidOTP, nil
case *sms.VerificationMessage:
return TypeVerificationValidOTP, nil
case *sms.TestStub:
return TypeTestStub, nil
default:
Expand All @@ -28,12 +30,18 @@ func SMSTemplateType(t SMSTemplate) (TemplateType, error) {

func NewSMSTemplateFromMessage(d Dependencies, m Message) (SMSTemplate, error) {
switch m.TemplateType {
case TypeOTP:
var t sms.OTPMessageModel
case TypeRecoveryValidOTP:
var t sms.RecoveryMessageModel
if err := json.Unmarshal(m.TemplateData, &t); err != nil {
return nil, err
}
return sms.NewOTPMessage(d, &t), nil
return sms.NewRecoveryOTPMessage(d, &t), nil
case TypeVerificationValidOTP:
var t sms.VerificationMessageModel
if err := json.Unmarshal(m.TemplateData, &t); err != nil {
return nil, err
}
return sms.NewVerificationOTPMessage(d, &t), nil
case TypeTestStub:
var t sms.TestStubModel
if err := json.Unmarshal(m.TemplateData, &t); err != nil {
Expand Down
10 changes: 6 additions & 4 deletions courier/sms_templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import (

func TestSMSTemplateType(t *testing.T) {
for expectedType, tmpl := range map[courier.TemplateType]courier.SMSTemplate{
courier.TypeOTP: &sms.OTPMessage{},
courier.TypeTestStub: &sms.TestStub{},
courier.TypeVerificationValidOTP: &sms.VerificationMessage{},
courier.TypeRecoveryValidOTP: &sms.RecoveryMessage{},
courier.TypeTestStub: &sms.TestStub{},
} {
t.Run(fmt.Sprintf("case=%s", expectedType), func(t *testing.T) {
actualType, err := courier.SMSTemplateType(tmpl)
Expand All @@ -31,8 +32,9 @@ func TestNewSMSTemplateFromMessage(t *testing.T) {
ctx := context.Background()

for tmplType, expectedTmpl := range map[courier.TemplateType]courier.SMSTemplate{
courier.TypeOTP: sms.NewOTPMessage(reg, &sms.OTPMessageModel{To: "+12345678901"}),
courier.TypeTestStub: sms.NewTestStub(reg, &sms.TestStubModel{To: "+12345678901", Body: "test body"}),
courier.TypeVerificationValidOTP: sms.NewVerificationOTPMessage(reg, &sms.VerificationMessageModel{To: "+12345678901"}),
courier.TypeRecoveryValidOTP: sms.NewRecoveryOTPMessage(reg, &sms.RecoveryMessageModel{To: "+12345678901"}),
courier.TypeTestStub: sms.NewTestStub(reg, &sms.TestStubModel{To: "+12345678901", Body: "test body"}),
} {
t.Run(fmt.Sprintf("case=%s", tmplType), func(t *testing.T) {
tmplData, err := json.Marshal(expectedTmpl)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Hi,

please recover access to your account by entering following code:

{{ .Code }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Hi,

please recover access to your account by entering following code:

{{ .Code }}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Recover access to your account
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Your recovery code is: {{ .Code }}.
Don't share this code with anyone.

This file was deleted.

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

please verify your account by entering following code:

{{ .Code }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Hi,

please verify your account by entering following code:

{{ .Code }}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Please verify your email address
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Your verification code is: {{ .Code }}.
Don't share this code with anyone.
57 changes: 57 additions & 0 deletions courier/template/email/recovery_valid_otp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package email

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

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

type (
RecoveryValidOTP struct {
d template.Dependencies
m *RecoveryValidOTPModel
}
RecoveryValidOTPModel struct {
To string
Code string
Identity map[string]interface{}
}
)

func NewRecoveryValidOTP(d template.Dependencies, m *RecoveryValidOTPModel) *RecoveryValidOTP {
return &RecoveryValidOTP{d: d, m: m}
}

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

func (t *RecoveryValidOTP) EmailSubject(ctx context.Context) (string, error) {
templatesDir := os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx))
subject := t.d.CourierConfig().CourierTemplatesRecoveryValid(ctx).Subject

subject, err := template.LoadText(ctx, t.d, templatesDir, "otp/recovery/valid/email.subject.gotmpl", "otp/recovery/valid/email.subject*", t.m, subject)

return strings.TrimSpace(subject), err
}

func (t *RecoveryValidOTP) EmailBody(ctx context.Context) (string, error) {
templatesDir := os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx))
body := t.d.CourierConfig().CourierTemplatesRecoveryValid(ctx).Body.HTML

return template.LoadHTML(ctx, t.d, templatesDir, "otp/recovery/valid/email.body.gotmpl", "otp/recovery/valid/email.body*", t.m, body)
}

func (t *RecoveryValidOTP) EmailBodyPlaintext(ctx context.Context) (string, error) {
templatesDir := os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx))
bodyPlaintext := t.d.CourierConfig().CourierTemplatesRecoveryValid(ctx).Body.PlainText

return template.LoadText(ctx, t.d, templatesDir, "otp/recovery/valid/email.body.plaintext.gotmpl", "otp/recovery/valid/email.body.plaintext*", t.m, bodyPlaintext)
}

func (t *RecoveryValidOTP) MarshalJSON() ([]byte, error) {
return json.Marshal(t.m)
}
27 changes: 27 additions & 0 deletions courier/template/email/recovery_valid_otp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package email_test

import (
"context"
"testing"

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

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

t.Run("test=with courier templates directory", func(t *testing.T) {
_, reg := internal.NewFastRegistryWithMocks(t)
tpl := email.NewRecoveryValidOTP(reg, &email.RecoveryValidOTPModel{})

testhelpers.TestRendered(t, ctx, tpl)
})

t.Run("test=with remote resources", func(t *testing.T) {
testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/otp/recovery/valid", courier.TypeRecoveryValid)
})
}
57 changes: 57 additions & 0 deletions courier/template/email/verification_valid_otp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package email

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

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

type (
VerificationValidOTP struct {
d template.Dependencies
m *VerificationValidOTPModel
}
VerificationValidOTPModel struct {
To string
Code string
Identity map[string]interface{}
}
)

func NewVerificationValidOTP(d template.Dependencies, m *VerificationValidOTPModel) *VerificationValidOTP {
return &VerificationValidOTP{d: d, m: m}
}

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

func (t *VerificationValidOTP) EmailSubject(ctx context.Context) (string, error) {
templatesDir := os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx))
subject := t.d.CourierConfig().CourierTemplatesVerificationValid(ctx).Subject

subject, err := template.LoadText(ctx, t.d, templatesDir, "otp/verification/valid/email.subject.gotmpl", "otp/verification/valid/email.subject*", t.m, subject)

return strings.TrimSpace(subject), err
}

func (t *VerificationValidOTP) EmailBody(ctx context.Context) (string, error) {
templatesDir := os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx))
body := t.d.CourierConfig().CourierTemplatesVerificationValid(ctx).Body.HTML

return template.LoadHTML(ctx, t.d, templatesDir, "otp/verification/valid/email.body.gotmpl", "otp/verification/valid/email.body*", t.m, body)
}

func (t *VerificationValidOTP) EmailBodyPlaintext(ctx context.Context) (string, error) {
templatesDir := os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx))
plaintextBody := t.d.CourierConfig().CourierTemplatesVerificationValid(ctx).Body.PlainText

return template.LoadText(ctx, t.d, templatesDir, "otp/verification/valid/email.body.plaintext.gotmpl", "otp/verification/valid/email.body.plaintext*", t.m, plaintextBody)
}

func (t *VerificationValidOTP) MarshalJSON() ([]byte, error) {
return json.Marshal(t.m)
}
27 changes: 27 additions & 0 deletions courier/template/email/verification_valid_otp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package email_test

import (
"context"
"testing"

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

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

t.Run("test=with courier templates directory", func(t *testing.T) {
_, reg := internal.NewFastRegistryWithMocks(t)
tpl := email.NewVerificationValidOTP(reg, &email.VerificationValidOTPModel{})

testhelpers.TestRendered(t, ctx, tpl)
})

t.Run("test=with remote resources", func(t *testing.T) {
testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/otp/verification/valid", courier.TypeVerificationValid)
})
}
38 changes: 0 additions & 38 deletions courier/template/sms/otp.go

This file was deleted.

Loading