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: add sms verification for phone numbers #3649

Merged
merged 14 commits into from
Dec 28, 2023
Merged
31 changes: 31 additions & 0 deletions contrib/quickstart/kratos/phone-password/identity.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Person",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"phone": {
"type": "string",
"format": "tel",
"title": "Phone number",
"minLength": 3,
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
},
"verification": {
"via": "phone"
}
}
}
},
"required": ["phone"],
"additionalProperties": false
}
}
}
113 changes: 113 additions & 0 deletions contrib/quickstart/kratos/phone-password/kratos.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
version: v0.13.0

dsn: memory

serve:
public:
base_url: http://127.0.0.1:4433/
cors:
enabled: true
admin:
base_url: http://kratos:4434/

selfservice:
default_browser_return_url: http://127.0.0.1:4455/
allowed_return_urls:
- http://127.0.0.1:4455
- http://localhost:19006/Callback
- exp://localhost:8081/--/Callback

methods:
password:
enabled: true
totp:
config:
issuer: Kratos
enabled: true
lookup_secret:
enabled: true
link:
enabled: true
code:
enabled: true

flows:
error:
ui_url: http://127.0.0.1:4455/error

settings:
ui_url: http://127.0.0.1:4455/settings
privileged_session_max_age: 15m
required_aal: highest_available

recovery:
enabled: true
ui_url: http://127.0.0.1:4455/recovery
use: code

verification:
enabled: true
ui_url: http://127.0.0.1:4455/verification
use: code
after:
default_browser_return_url: http://127.0.0.1:4455/

logout:
after:
default_browser_return_url: http://127.0.0.1:4455/login

login:
ui_url: http://127.0.0.1:4455/login
lifespan: 10m

registration:
lifespan: 10m
ui_url: http://127.0.0.1:4455/registration
after:
password:
hooks:
- hook: session
- hook: show_verification_ui

log:
level: debug
format: text
leak_sensitive_values: true

secrets:
cookie:
- PLEASE-CHANGE-ME-I-AM-VERY-INSECURE
cipher:
- 32-LONG-SECRET-NOT-SECURE-AT-ALL

ciphers:
algorithm: xchacha20-poly1305

hashers:
algorithm: bcrypt
bcrypt:
cost: 8

identity:
default_schema_id: default
schemas:
- id: default
url: file:///etc/config/kratos/identity.schema.json

courier:
channels:
- id: phone
request_config:
url: https://api.twilio.com/2010-04-01/Accounts/AXXXXXXXXXXXXXX/Messages.json
method: POST
body: base64://ZnVuY3Rpb24oY3R4KSB7CkJvZHk6IGN0eC5ib2R5LApUbzogY3R4LnRvLEZyb206IGN0eC5mcm9tCn0=
headers:
Content-Type: application/x-www-form-urlencoded
auth:
type: basic_auth
config:
user: AXXXXXXX
password: XXXX

feature_flags:
use_continue_with_transitions: true
13 changes: 13 additions & 0 deletions courier/channel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package courier

import (
"context"
)

type Channel interface {
ID() string
Dispatch(ctx context.Context, msg Message) error
}
40 changes: 26 additions & 14 deletions courier/courier.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,15 @@
"context"
"time"

"github.com/ory/kratos/courier/template"
"github.com/ory/x/jsonnetsecure"

"github.com/cenkalti/backoff"
"github.com/gofrs/uuid"
"github.com/pkg/errors"

"github.com/ory/kratos/courier/template"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/x"
gomail "github.com/ory/mail/v3"
)

type (
Expand All @@ -33,11 +32,8 @@
Work(ctx context.Context) error
QueueEmail(ctx context.Context, t EmailTemplate) (uuid.UUID, error)
QueueSMS(ctx context.Context, t SMSTemplate) (uuid.UUID, error)
SmtpDialer() *gomail.Dialer
DispatchQueue(ctx context.Context) error
DispatchMessage(ctx context.Context, msg Message) error
SetGetEmailTemplateType(f func(t EmailTemplate) (TemplateType, error))
SetNewEmailTemplateFromMessage(f func(d template.Dependencies, msg Message) (EmailTemplate, error))
UseBackoff(b backoff.BackOff)
FailOnDispatchError()
}
Expand All @@ -51,26 +47,42 @@
}

courier struct {
smsClient *smsClient
smtpClient *smtpClient
httpClient *httpClient
courierChannels map[string]Channel
deps Dependencies
failOnDispatchError bool
backoff backoff.BackOff
}
)

func NewCourier(ctx context.Context, deps Dependencies) (Courier, error) {
smtp, err := newSMTP(ctx, deps)
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

Check warning on line 72 in courier/courier.go

View check run for this annotation

Codecov / codecov/patch

courier/courier.go#L72

Added line #L72 was not covered by tests
}
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)

Check warning on line 78 in courier/courier.go

View check run for this annotation

Codecov / codecov/patch

courier/courier.go#L77-L78

Added lines #L77 - L78 were not covered by tests
}
}

return &courier{
smsClient: newSMS(ctx, deps),
smtpClient: smtp,
httpClient: newHTTP(ctx, deps),
deps: deps,
backoff: backoff.NewExponentialBackOff(),
deps: deps,
backoff: backoff.NewExponentialBackOff(),
courierChannels: channels,
}, nil
}

Expand Down
20 changes: 9 additions & 11 deletions courier/courier_dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,21 @@ func (c *courier) DispatchMessage(ctx context.Context, msg Message) error {
return err
}

switch msg.Type {
case MessageTypeEmail:
if err := c.dispatchEmail(ctx, msg); err != nil {
return err
}
case MessageTypePhone:
if err := c.dispatchSMS(ctx, msg); err != nil {
return err
}
default:
return errors.Errorf("received unexpected message type: %d", msg.Type)
channel, ok := c.courierChannels[msg.Channel.String()]
if !ok {
return errors.Errorf("message %s has unknown channel %q", msg.ID.String(), msg.Channel)
}

if err := channel.Dispatch(ctx, msg); err != nil {
return err
}

if err := c.deps.CourierPersister().SetMessageStatus(ctx, msg.ID, MessageStatusSent); err != nil {
c.deps.Logger().
WithError(err).
WithField("message_id", msg.ID).
WithField("message_nid", msg.NID).
WithField("channel", channel.ID()).
Error(`Unable to set the message status to "sent".`)
return err
}
Expand All @@ -47,6 +44,7 @@ func (c *courier) DispatchMessage(ctx context.Context, msg Message) error {
WithField("message_type", msg.Type).
WithField("message_template_type", msg.TemplateType).
WithField("message_subject", msg.Subject).
WithField("channel", channel.ID()).
Debug("Courier sent out message.")

return nil
Expand Down
28 changes: 28 additions & 0 deletions courier/courier_dispatcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
templates "github.com/ory/kratos/courier/template/email"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/internal"
"github.com/ory/kratos/internal/testhelpers"
)

func queueNewMessage(t *testing.T, ctx context.Context, c courier.Courier, d template.Dependencies) uuid.UUID {
Expand Down Expand Up @@ -58,6 +59,33 @@ func TestDispatchMessageWithInvalidSMTP(t *testing.T) {
})
}

func TestDispatchMessage(t *testing.T) {
ctx := context.Background()

conf, reg := internal.NewRegistryDefaultWithDSN(t, "")
conf.MustSet(ctx, config.ViperKeyCourierMessageRetries, 5)
conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "http://foo.url")

ctx, cancel := context.WithCancel(ctx)
t.Cleanup(cancel)

c, err := reg.Courier(ctx)
require.NoError(t, err)
t.Run("case=invalid channel", func(t *testing.T) {
message := courier.Message{
Channel: "invalid-channel",
Status: courier.MessageStatusQueued,
Type: courier.MessageTypeEmail,
Recipient: testhelpers.RandomEmail(),
Subject: "test-subject-1",
Body: "test-body-1",
TemplateType: "stub",
}
require.NoError(t, reg.CourierPersister().AddMessage(ctx, &message))
require.Error(t, c.DispatchMessage(ctx, message))
})
}

func TestDispatchQueue(t *testing.T) {
ctx := context.Background()

Expand Down
Loading
Loading