Skip to content

Commit

Permalink
feat: support MFA via SMS (#3682)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: zepatrik <[email protected]>
  • Loading branch information
jonas-jonas and zepatrik authored Jan 26, 2024
1 parent f1493c8 commit 1516cf6
Show file tree
Hide file tree
Showing 67 changed files with 1,473 additions and 509 deletions.
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -175,15 +175,15 @@ docker:
DOCKER_BUILDKIT=1 DOCKER_CONTENT_TRUST=1 docker build -f .docker/Dockerfile-build --build-arg=COMMIT=$(VCS_REF) --build-arg=BUILD_DATE=$(BUILD_DATE) -t oryd/kratos:${IMAGE_TAG} .

.PHONY: test-e2e
test-e2e: node_modules test-resetdb
test-e2e: node_modules test-resetdb kratos-config-e2e
source script/test-envs.sh
test/e2e/run.sh sqlite
test/e2e/run.sh postgres
test/e2e/run.sh cockroach
test/e2e/run.sh mysql

.PHONY: test-e2e-playwright
test-e2e-playwright: node_modules test-resetdb
test-e2e-playwright: node_modules test-resetdb kratos-config-e2e
source script/test-envs.sh
test/e2e/run.sh --only-setup
(cd test/e2e; DB=memory npm run playwright)
Expand Down Expand Up @@ -212,3 +212,7 @@ licenses: .bin/licenses node_modules # checks open-source licenses
node_modules: package-lock.json
npm ci
touch node_modules

.PHONY: kratos-config-e2e
kratos-config-e2e:
sh ./test/e2e/render-kratos-config.sh
7 changes: 5 additions & 2 deletions cmd/clidoc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ func init() {
"NewErrorValidationRegistrationRetrySuccessful": text.NewErrorValidationRegistrationRetrySuccessful(),
"NewInfoSelfServiceRegistrationRegisterCode": text.NewInfoSelfServiceRegistrationRegisterCode(),
"NewErrorValidationLoginLinkedCredentialsDoNotMatch": text.NewErrorValidationLoginLinkedCredentialsDoNotMatch(),
"NewErrorValidationAddressUnknown": text.NewErrorValidationAddressUnknown(),
"NewInfoSelfServiceLoginCodeMFA": text.NewInfoSelfServiceLoginCodeMFA(),
"NewInfoSelfServiceLoginCodeMFAHint": text.NewInfoSelfServiceLoginCodeMFAHint("{maskedIdentifier}"),
}
}

Expand Down Expand Up @@ -247,7 +250,7 @@ func writeMessages(path string, sortedMessages []*text.Message) error {
r := regexp.MustCompile(`(?s)<!-- START MESSAGE TABLE -->(.*?)<!-- END MESSAGE TABLE -->`)
result := r.ReplaceAllString(string(content), "<!-- START MESSAGE TABLE -->\n"+w.String()+"\n<!-- END MESSAGE TABLE -->")

f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o755)
if err != nil {
return err
}
Expand All @@ -266,7 +269,7 @@ func writeMessages(path string, sortedMessages []*text.Message) error {
func writeMessagesJson(path string, sortedMessages []*text.Message) error {
result := codeEncode(sortedMessages)

f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o755)
if err != nil {
return err
}
Expand Down
24 changes: 22 additions & 2 deletions contrib/quickstart/kratos/phone-password/identity.schema.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
{
"$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json",
"$id": "https://schemas.ory.sh/presets/kratos/quickstart/phone-password/identity.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Person",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "E-mail",
"minLength": 3,
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
},
"code": {
"identifier": true,
"via": "email"
}
},
"verification": {
"via": "email"
}
}
},
"phone": {
"type": "string",
"format": "tel",
Expand All @@ -24,7 +44,7 @@
}
}
},
"required": ["phone"],
"required": ["email", "phone"],
"additionalProperties": false
}
}
Expand Down
6 changes: 6 additions & 0 deletions courier/sms_templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ func NewSMSTemplateFromMessage(d template.Dependencies, m Message) (SMSTemplate,
return nil, err
}
return sms.NewTestStub(d, &t), nil
case template.TypeLoginCodeValid:
var t sms.LoginCodeValidModel
if err := json.Unmarshal(m.TemplateData, &t); err != nil {
return nil, err
}
return sms.NewLoginCodeValid(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 login code is: {{ .LoginCode }}
52 changes: 52 additions & 0 deletions courier/template/sms/login_code_valid.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 (
LoginCodeValid struct {
deps template.Dependencies
model *LoginCodeValidModel
}
LoginCodeValidModel struct {
To string
LoginCode string
Identity map[string]interface{}
}
)

func NewLoginCodeValid(d template.Dependencies, m *LoginCodeValidModel) *LoginCodeValid {
return &LoginCodeValid{deps: d, model: m}
}

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

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

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

func (t *LoginCodeValid) TemplateType() template.TemplateType {
return template.TypeLoginCodeValid
}
37 changes: 37 additions & 0 deletions courier/template/sms/login_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 TestNewLoginCodeValid(t *testing.T) {
_, reg := internal.NewFastRegistryWithMocks(t)

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

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

expectedBody := fmt.Sprintf("Your login 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)
}
18 changes: 17 additions & 1 deletion driver/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ const (
ViperKeyCourierTemplatesVerificationCodeInvalidEmail = "courier.templates.verification_code.invalid.email"
ViperKeyCourierTemplatesVerificationCodeValidEmail = "courier.templates.verification_code.valid.email"
ViperKeyCourierTemplatesVerificationCodeValidSMS = "courier.templates.verification_code.valid.sms"
ViperKeyCourierTemplatesLoginCodeValidSMS = "courier.templates.login_code.valid.sms"
ViperKeyCourierDeliveryStrategy = "courier.delivery_strategy"
ViperKeyCourierHTTPRequestConfig = "courier.http.request_config"
ViperKeyCourierTemplatesLoginCodeValidEmail = "courier.templates.login_code.valid.email"
Expand Down Expand Up @@ -236,6 +237,7 @@ type (
SelfServiceStrategyCode struct {
*SelfServiceStrategy
PasswordlessEnabled bool `json:"passwordless_enabled"`
MFAEnabled bool `json:"mfa_enabled"`
}
Schema struct {
ID string `json:"id" koanf:"id"`
Expand Down Expand Up @@ -303,6 +305,7 @@ type (
CourierTemplatesLoginCodeValid(ctx context.Context) *CourierEmailTemplate
CourierTemplatesRegistrationCodeValid(ctx context.Context) *CourierEmailTemplate
CourierSMSTemplatesVerificationCodeValid(ctx context.Context) *CourierSMSTemplate
CourierSMSTemplatesLoginCodeValid(ctx context.Context) *CourierSMSTemplate
CourierMessageRetries(ctx context.Context) int
CourierWorkerPullCount(ctx context.Context) int
CourierWorkerPullWait(ctx context.Context) time.Duration
Expand Down Expand Up @@ -758,8 +761,16 @@ func (p *Config) SelfServiceStrategy(ctx context.Context, strategy string) *Self
case "code", "password", "profile":
defaultEnabled = true
}

// Backwards compatibility for the old "passwordless_enabled" key
// This force-enables the code strategy, if passwordless is enabled, because in earlier versions it was possible to
// disable the code strategy, but enable passwordless
enabled := pp.BoolF(basePath+".enabled", defaultEnabled)
if strategy == "code" {
enabled = enabled || pp.Bool(basePath+".passwordless_enabled")
}
return &SelfServiceStrategy{
Enabled: pp.BoolF(basePath+".enabled", defaultEnabled),
Enabled: enabled,
Config: config,
}
}
Expand All @@ -782,6 +793,7 @@ func (p *Config) SelfServiceCodeStrategy(ctx context.Context) *SelfServiceStrate
Config: config,
},
PasswordlessEnabled: pp.BoolF(basePath+".passwordless_enabled", false),
MFAEnabled: pp.BoolF(basePath+".mfa_enabled", false),
}
}

Expand Down Expand Up @@ -1115,6 +1127,10 @@ func (p *Config) CourierSMSTemplatesVerificationCodeValid(ctx context.Context) *
return p.CourierSMSTemplatesHelper(ctx, ViperKeyCourierTemplatesVerificationCodeValidSMS)
}

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

func (p *Config) CourierTemplatesLoginCodeValid(ctx context.Context) *CourierEmailTemplate {
return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesLoginCodeValidEmail)
}
Expand Down
14 changes: 2 additions & 12 deletions driver/registry_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,21 +327,11 @@ func (m *RegistryDefault) selfServiceStrategies() []any {
}

func (m *RegistryDefault) strategyRegistrationEnabled(ctx context.Context, id string) bool {
switch id {
case identity.CredentialsTypeCodeAuth.String():
return m.Config().SelfServiceCodeStrategy(ctx).PasswordlessEnabled
default:
return m.Config().SelfServiceStrategy(ctx, id).Enabled
}
return m.Config().SelfServiceStrategy(ctx, id).Enabled
}

func (m *RegistryDefault) strategyLoginEnabled(ctx context.Context, id string) bool {
switch id {
case identity.CredentialsTypeCodeAuth.String():
return m.Config().SelfServiceCodeStrategy(ctx).PasswordlessEnabled
default:
return m.Config().SelfServiceStrategy(ctx, id).Enabled
}
return m.Config().SelfServiceStrategy(ctx, id).Enabled
}

func (m *RegistryDefault) RegistrationStrategies(ctx context.Context, filters ...registration.StrategyFilter) (registrationStrategies registration.Strategies) {
Expand Down
Loading

0 comments on commit 1516cf6

Please sign in to comment.