Skip to content

Commit

Permalink
feat: allow SMS as passwordless login method
Browse files Browse the repository at this point in the history
This patch allows Ory Kratos to use the SMS gateway for login and registration with code via SMS.
  • Loading branch information
aeneasr committed Sep 16, 2024
1 parent 7d6a458 commit e6f449f
Show file tree
Hide file tree
Showing 15 changed files with 123 additions and 16 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
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
@@ -0,0 +1 @@
Your registration code is: {{ .RegistrationCode }}
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"`
Identity map[string]interface{} `json:"identity"`
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().CourierSMSTemplatesRegistrationCodeValid(ctx).Body.PlainText,
)
}

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

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

package sms_test

import (
"context"
"fmt"
"testing"

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

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

func TestNewRegistrationCodeValid(t *testing.T) {
_, reg := internal.NewFastRegistryWithMocks(t)

const (
expectedPhone = "+12345678901"
otp = "012345"
)

tpl := sms.NewRegistrationCodeValid(reg, &sms.RegistrationCodeValidModel{To: expectedPhone, RegistrationCode: otp})

expectedBody := fmt.Sprintf("Your registration code is: %s\n", otp)

actualBody, err := tpl.SMSBody(context.Background())
require.NoError(t, err)
assert.Equal(t, expectedBody, actualBody)

actualPhone, err := tpl.PhoneNumber()
require.NoError(t, err)
assert.Equal(t, expectedPhone, actualPhone)
}
6 changes: 6 additions & 0 deletions driver/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const (
ViperKeyCourierTemplatesVerificationCodeValidEmail = "courier.templates.verification_code.valid.email"
ViperKeyCourierTemplatesVerificationCodeValidSMS = "courier.templates.verification_code.valid.sms"
ViperKeyCourierTemplatesLoginCodeValidSMS = "courier.templates.login_code.valid.sms"
ViperKeyCourierTemplatesRegistrationCodeValidSMS = "courier.templates.registration_code.valid.sms"
ViperKeyCourierDeliveryStrategy = "courier.delivery_strategy"
ViperKeyCourierHTTPRequestConfig = "courier.http.request_config"
ViperKeyCourierTemplatesLoginCodeValidEmail = "courier.templates.login_code.valid.email"
Expand Down Expand Up @@ -322,6 +323,7 @@ type (
CourierTemplatesRegistrationCodeValid(ctx context.Context) *CourierEmailTemplate
CourierSMSTemplatesVerificationCodeValid(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 @@ -1160,6 +1162,10 @@ func (p *Config) CourierSMSTemplatesLoginCodeValid(ctx context.Context) *Courier
return p.CourierSMSTemplatesHelper(ctx, ViperKeyCourierTemplatesLoginCodeValidSMS)
}

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

func (p *Config) CourierTemplatesLoginCodeValid(ctx context.Context) *CourierEmailTemplate {
return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesLoginCodeValidEmail)
}
Expand Down
3 changes: 3 additions & 0 deletions embedx/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2043,6 +2043,9 @@
"properties": {
"email": {
"$ref": "#/definitions/emailCourierTemplate"
},
"sms": {
"$ref": "#/definitions/smsCourierTemplate"
}
},
"required": ["email"]
Expand Down
2 changes: 1 addition & 1 deletion selfservice/strategy/code/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ func (s *Strategy) populateEmailSentFlow(ctx context.Context, f flow.Flow) error
case flow.LoginFlow:
route = login.RouteSubmitFlow
codeMetaLabel = text.NewInfoNodeLabelLoginCode()
message = text.NewLoginEmailWithCodeSent()
message = text.NewLoginCodeSent()

// preserve the login identifier that was submitted
// so we can retry the code flow with the same data
Expand Down
2 changes: 1 addition & 1 deletion selfservice/strategy/code/strategy_registration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ func TestRegistrationCodeStrategy(t *testing.T) {
} else {
require.NotEmptyf(t, csrfToken, "expected to find the csrf_token but got %s", body)
}
require.Containsf(t, gjson.Get(body, "ui.messages").String(), "An email containing a code has been sent to the email address you provided.", "%s", body)
require.Containsf(t, gjson.Get(body, "ui.messages").String(), "A code has been sent to the address(es) you provided", "%s", body)
})

// get the new code from email
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ context("Login error messages with code method", () => {

cy.get('[data-testid="ui/message/1010014"]').should(
"contain",
"An email containing a code has been sent to the email address you provided",
"A code has been sent to the address you provided",
)

cy.get(Selectors[app]["code"]).type("123456")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ context("Registration error messages with code method", () => {
cy.submitCodeForm(app)
cy.get('[data-testid="ui/message/1040005"]').should(
"contain",
"An email containing a code has been sent to the email address you provided",
"A code has been sent to the address(es) you provided",
)

if (app !== "express") {
Expand Down Expand Up @@ -133,7 +133,7 @@ context("Registration error messages with code method", () => {
cy.submitCodeForm(app)
cy.get('[data-testid="ui/message/1040005"]').should(
"contain",
"An email containing a code has been sent to the email address you provided",
"A code has been sent to the address(es) you provided",
)

cy.removeAttribute([Selectors[app]["code"]], "required")
Expand Down Expand Up @@ -202,7 +202,7 @@ context("Registration error messages with code method", () => {
cy.submitCodeForm(app)
cy.get('[data-testid="ui/message/1040005"]').should(
"contain",
"An email containing a code has been sent to the email address you provided",
"A code has been sent to the address(es) you provided",
)

cy.getRegistrationCodeFromEmail(email).then((code) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ context("Registration success with code method", () => {
cy.submitCodeForm(app)
cy.get('[data-testid="ui/message/1040005"]').should(
"contain",
"An email containing a code has been sent to the email address you provided",
"A code has been sent to the address(es) you provided",
)

cy.getRegistrationCodeFromEmail(email).then((code) =>
Expand Down Expand Up @@ -189,7 +189,7 @@ context("Registration success with code method", () => {
cy.submitCodeForm(app)
cy.get('[data-testid="ui/message/1040005"]').should(
"contain",
"An email containing a code has been sent to the email address you provided",
"A code has been sent to the address(es) you provided",
)

cy.getRegistrationCodeFromEmail(email).should((code) => {
Expand Down Expand Up @@ -236,7 +236,7 @@ context("Registration success with code method", () => {
cy.submitCodeForm(app)
cy.get('[data-testid="ui/message/1040005"]').should(
"contain",
"An email containing a code has been sent to the email address you provided",
"A code has been sent to the address(es) you provided",
)

cy.getRegistrationCodeFromEmail(email).should((code) => {
Expand Down Expand Up @@ -307,7 +307,7 @@ context("Registration success with code method", () => {
cy.submitCodeForm(app)
cy.get('[data-testid="ui/message/1040005"]').should(
"contain",
"An email containing a code has been sent to the email address you provided",
"A code has been sent to the address(es) you provided",
)

// intentionally use email 1 to sign up for the account
Expand Down
2 changes: 1 addition & 1 deletion text/id.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const (
InfoSelfServiceLoginContinueWebAuthn // 1010011
InfoSelfServiceLoginWebAuthnPasswordless // 1010012
InfoSelfServiceLoginContinue // 1010013
InfoSelfServiceLoginEmailWithCodeSent // 1010014
InfoSelfServiceLoginCodeSent // 1010014
InfoSelfServiceLoginCode // 1010015
InfoSelfServiceLoginLink // 1010016
InfoSelfServiceLoginAndLink // 1010017
Expand Down
6 changes: 3 additions & 3 deletions text/message_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,11 +224,11 @@ func NewInfoSelfServiceLoginContinue() *Message {
}
}

func NewLoginEmailWithCodeSent() *Message {
func NewLoginCodeSent() *Message {
return &Message{
ID: InfoSelfServiceLoginEmailWithCodeSent,
ID: InfoSelfServiceLoginCodeSent,
Type: Info,
Text: "An email containing a code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and retry the login.",
Text: "A code has been sent to the address you provided. If you have not received an message, check the spelling of the address and retry the login.",
}
}

Expand Down
2 changes: 1 addition & 1 deletion text/message_registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func NewRegistrationEmailWithCodeSent() *Message {
return &Message{
ID: InfoSelfServiceRegistrationEmailWithCodeSent,
Type: Info,
Text: "An email containing a code has been sent to the email address you provided. If you have not received an email, check the spelling of the address and retry the registration.",
Text: "A code has been sent to the address(es) you provided. If you have not received an message, check the spelling of the address and retry the registration.",
}
}

Expand Down

0 comments on commit e6f449f

Please sign in to comment.