Skip to content

Commit a82ee92

Browse files
authored
feat: add verification via code (#2838)
The new `code` strategy is now supported as a verification strategy. If enabled, the strategy sends a code, instead of a magic link to the user's address, which they can use to verify their address. Close #2824
1 parent a318778 commit a82ee92

File tree

88 files changed

+4270
-564
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+4270
-564
lines changed

.vscode/launch.json

-15
This file was deleted.

cmd/clidoc/main.go

+2
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ func init() {
7575
"NewErrorValidationVerificationTokenInvalidOrAlreadyUsed": text.NewErrorValidationVerificationTokenInvalidOrAlreadyUsed(),
7676
"NewErrorValidationVerificationRetrySuccess": text.NewErrorValidationVerificationRetrySuccess(),
7777
"NewErrorValidationVerificationStateFailure": text.NewErrorValidationVerificationStateFailure(),
78+
"NewErrorValidationVerificationCodeInvalidOrAlreadyUsed": text.NewErrorValidationVerificationCodeInvalidOrAlreadyUsed(),
7879
"NewErrorSystemGeneric": text.NewErrorSystemGeneric("{reason}"),
7980
"NewValidationErrorGeneric": text.NewValidationErrorGeneric("{reason}"),
8081
"NewValidationErrorRequired": text.NewValidationErrorRequired("{field}"),
@@ -121,6 +122,7 @@ func init() {
121122
"NewErrorValidationRecoveryStateFailure": text.NewErrorValidationRecoveryStateFailure(),
122123
"NewInfoNodeInputEmail": text.NewInfoNodeInputEmail(),
123124
"NewInfoNodeResendOTP": text.NewInfoNodeResendOTP(),
125+
"NewInfoNodeLabelReturn": text.NewInfoNodeLabelReturn(),
124126
"NewInfoSelfServiceSettingsRegisterWebAuthn": text.NewInfoSelfServiceSettingsRegisterWebAuthn(),
125127
"NewInfoLoginWebAuthnPasswordless": text.NewInfoLoginWebAuthnPasswordless(),
126128
"NewInfoSelfServiceRegistrationRegisterWebAuthn": text.NewInfoSelfServiceRegistrationRegisterWebAuthn(),

contrib/quickstart/kratos/email-password/kratos.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ selfservice:
2525
lookup_secret:
2626
enabled: true
2727
link:
28-
enabled: false
28+
enabled: true
2929
code:
3030
enabled: true
3131

@@ -46,6 +46,7 @@ selfservice:
4646
verification:
4747
enabled: true
4848
ui_url: http://127.0.0.1:4455/verification
49+
use: code
4950
after:
5051
default_browser_return_url: http://127.0.0.1:4455/
5152

courier/email_templates.go

+26-8
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,16 @@ type (
3030
type TemplateType string
3131

3232
const (
33-
TypeRecoveryInvalid TemplateType = "recovery_invalid"
34-
TypeRecoveryValid TemplateType = "recovery_valid"
35-
TypeRecoveryCodeInvalid TemplateType = "recovery_code_invalid"
36-
TypeRecoveryCodeValid TemplateType = "recovery_code_valid"
37-
TypeVerificationInvalid TemplateType = "verification_invalid"
38-
TypeVerificationValid TemplateType = "verification_valid"
39-
TypeOTP TemplateType = "otp"
40-
TypeTestStub TemplateType = "stub"
33+
TypeRecoveryInvalid TemplateType = "recovery_invalid"
34+
TypeRecoveryValid TemplateType = "recovery_valid"
35+
TypeRecoveryCodeInvalid TemplateType = "recovery_code_invalid"
36+
TypeRecoveryCodeValid TemplateType = "recovery_code_valid"
37+
TypeVerificationInvalid TemplateType = "verification_invalid"
38+
TypeVerificationValid TemplateType = "verification_valid"
39+
TypeVerificationCodeInvalid TemplateType = "verification_code_invalid"
40+
TypeVerificationCodeValid TemplateType = "verification_code_valid"
41+
TypeOTP TemplateType = "otp"
42+
TypeTestStub TemplateType = "stub"
4143
)
4244

4345
func GetEmailTemplateType(t EmailTemplate) (TemplateType, error) {
@@ -54,6 +56,10 @@ func GetEmailTemplateType(t EmailTemplate) (TemplateType, error) {
5456
return TypeVerificationInvalid, nil
5557
case *email.VerificationValid:
5658
return TypeVerificationValid, nil
59+
case *email.VerificationCodeInvalid:
60+
return TypeVerificationCodeInvalid, nil
61+
case *email.VerificationCodeValid:
62+
return TypeVerificationCodeValid, nil
5763
case *email.TestStub:
5864
return TypeTestStub, nil
5965
default:
@@ -99,6 +105,18 @@ func NewEmailTemplateFromMessage(d template.Dependencies, msg Message) (EmailTem
99105
return nil, err
100106
}
101107
return email.NewVerificationValid(d, &t), nil
108+
case TypeVerificationCodeInvalid:
109+
var t email.VerificationCodeInvalidModel
110+
if err := json.Unmarshal(msg.TemplateData, &t); err != nil {
111+
return nil, err
112+
}
113+
return email.NewVerificationCodeInvalid(d, &t), nil
114+
case TypeVerificationCodeValid:
115+
var t email.VerificationCodeValidModel
116+
if err := json.Unmarshal(msg.TemplateData, &t); err != nil {
117+
return nil, err
118+
}
119+
return email.NewVerificationCodeValid(d, &t), nil
102120
case TypeTestStub:
103121
var t email.TestStubModel
104122
if err := json.Unmarshal(msg.TemplateData, &t); err != nil {

courier/email_templates_test.go

+18-12
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,15 @@ import (
1818

1919
func TestGetTemplateType(t *testing.T) {
2020
for expectedType, tmpl := range map[courier.TemplateType]courier.EmailTemplate{
21-
courier.TypeRecoveryInvalid: &email.RecoveryInvalid{},
22-
courier.TypeRecoveryValid: &email.RecoveryValid{},
23-
courier.TypeVerificationInvalid: &email.VerificationInvalid{},
24-
courier.TypeVerificationValid: &email.VerificationValid{},
25-
courier.TypeTestStub: &email.TestStub{},
21+
courier.TypeRecoveryInvalid: &email.RecoveryInvalid{},
22+
courier.TypeRecoveryValid: &email.RecoveryValid{},
23+
courier.TypeRecoveryCodeInvalid: &email.RecoveryCodeInvalid{},
24+
courier.TypeRecoveryCodeValid: &email.RecoveryCodeValid{},
25+
courier.TypeVerificationInvalid: &email.VerificationInvalid{},
26+
courier.TypeVerificationValid: &email.VerificationValid{},
27+
courier.TypeVerificationCodeInvalid: &email.VerificationCodeInvalid{},
28+
courier.TypeVerificationCodeValid: &email.VerificationCodeValid{},
29+
courier.TypeTestStub: &email.TestStub{},
2630
} {
2731
t.Run(fmt.Sprintf("case=%s", expectedType), func(t *testing.T) {
2832
actualType, err := courier.GetEmailTemplateType(tmpl)
@@ -37,13 +41,15 @@ func TestNewEmailTemplateFromMessage(t *testing.T) {
3741
ctx := context.Background()
3842

3943
for tmplType, expectedTmpl := range map[courier.TemplateType]courier.EmailTemplate{
40-
courier.TypeRecoveryInvalid: email.NewRecoveryInvalid(reg, &email.RecoveryInvalidModel{To: "foo"}),
41-
courier.TypeRecoveryValid: email.NewRecoveryValid(reg, &email.RecoveryValidModel{To: "bar", RecoveryURL: "http://foo.bar"}),
42-
courier.TypeRecoveryCodeValid: email.NewRecoveryCodeValid(reg, &email.RecoveryCodeValidModel{To: "bar", RecoveryCode: "12345678"}),
43-
courier.TypeRecoveryCodeInvalid: email.NewRecoveryCodeInvalid(reg, &email.RecoveryCodeInvalidModel{To: "bar"}),
44-
courier.TypeVerificationInvalid: email.NewVerificationInvalid(reg, &email.VerificationInvalidModel{To: "baz"}),
45-
courier.TypeVerificationValid: email.NewVerificationValid(reg, &email.VerificationValidModel{To: "faz", VerificationURL: "http://bar.foo"}),
46-
courier.TypeTestStub: email.NewTestStub(reg, &email.TestStubModel{To: "far", Subject: "test subject", Body: "test body"}),
44+
courier.TypeRecoveryInvalid: email.NewRecoveryInvalid(reg, &email.RecoveryInvalidModel{To: "foo"}),
45+
courier.TypeRecoveryValid: email.NewRecoveryValid(reg, &email.RecoveryValidModel{To: "bar", RecoveryURL: "http://foo.bar"}),
46+
courier.TypeRecoveryCodeValid: email.NewRecoveryCodeValid(reg, &email.RecoveryCodeValidModel{To: "bar", RecoveryCode: "12345678"}),
47+
courier.TypeRecoveryCodeInvalid: email.NewRecoveryCodeInvalid(reg, &email.RecoveryCodeInvalidModel{To: "bar"}),
48+
courier.TypeVerificationInvalid: email.NewVerificationInvalid(reg, &email.VerificationInvalidModel{To: "baz"}),
49+
courier.TypeVerificationValid: email.NewVerificationValid(reg, &email.VerificationValidModel{To: "faz", VerificationURL: "http://bar.foo"}),
50+
courier.TypeVerificationCodeInvalid: email.NewVerificationCodeInvalid(reg, &email.VerificationCodeInvalidModel{To: "baz"}),
51+
courier.TypeVerificationCodeValid: email.NewVerificationCodeValid(reg, &email.VerificationCodeValidModel{To: "faz", VerificationURL: "http://bar.foo", VerificationCode: "123456678"}),
52+
courier.TypeTestStub: email.NewTestStub(reg, &email.TestStubModel{To: "far", Subject: "test subject", Body: "test body"}),
4753
} {
4854
t.Run(fmt.Sprintf("case=%s", tmplType), func(t *testing.T) {
4955
tmplData, err := json.Marshal(expectedTmpl)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Hi,
2+
3+
someone asked to verify this email address, but we were unable to find an account for this address.
4+
5+
If this was you, check if you signed up using a different address.
6+
7+
If this was not you, please ignore this email.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Hi,
2+
3+
someone asked to verify this email address, but we were unable to find an account for this address.
4+
5+
If this was you, check if you signed up using a different address.
6+
7+
If this was not you, please ignore this email.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Someone tried to verify this email address
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Hi,
2+
3+
please verify your account by entering the following code:
4+
5+
{{ .VerificationCode }}
6+
7+
or clicking the following link:
8+
9+
<a href="{{ .VerificationURL }}">{{ .VerificationURL }}</a>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Hi,
2+
3+
please verify your account by entering the following code:
4+
5+
{{ .VerificationCode }}
6+
7+
or clicking the following link:
8+
9+
{{ .VerificationURL }}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Please verify your email address
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright © 2022 Ory Corp
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package email
5+
6+
import (
7+
"context"
8+
"encoding/json"
9+
"os"
10+
"strings"
11+
12+
"github.com/ory/kratos/courier/template"
13+
)
14+
15+
type (
16+
VerificationCodeInvalid struct {
17+
d template.Dependencies
18+
m *VerificationCodeInvalidModel
19+
}
20+
VerificationCodeInvalidModel struct {
21+
To string
22+
}
23+
)
24+
25+
func NewVerificationCodeInvalid(d template.Dependencies, m *VerificationCodeInvalidModel) *VerificationCodeInvalid {
26+
return &VerificationCodeInvalid{d: d, m: m}
27+
}
28+
29+
func (t *VerificationCodeInvalid) EmailRecipient() (string, error) {
30+
return t.m.To, nil
31+
}
32+
33+
func (t *VerificationCodeInvalid) EmailSubject(ctx context.Context) (string, error) {
34+
subject, err := template.LoadText(
35+
ctx,
36+
t.d,
37+
os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)),
38+
"verification_code/invalid/email.subject.gotmpl",
39+
"verification_code/invalid/email.subject*",
40+
t.m,
41+
t.d.CourierConfig().CourierTemplatesVerificationCodeInvalid(ctx).Subject,
42+
)
43+
44+
return strings.TrimSpace(subject), err
45+
}
46+
47+
func (t *VerificationCodeInvalid) EmailBody(ctx context.Context) (string, error) {
48+
return template.LoadHTML(
49+
ctx,
50+
t.d,
51+
os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)),
52+
"verification_code/invalid/email.body.gotmpl",
53+
"verification_code/invalid/email.body*",
54+
t.m,
55+
t.d.CourierConfig().CourierTemplatesVerificationCodeInvalid(ctx).Body.HTML,
56+
)
57+
}
58+
59+
func (t *VerificationCodeInvalid) EmailBodyPlaintext(ctx context.Context) (string, error) {
60+
return template.LoadText(
61+
ctx,
62+
t.d,
63+
os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)),
64+
"verification_code/invalid/email.body.plaintext.gotmpl",
65+
"verification_code/invalid/email.body.plaintext*",
66+
t.m,
67+
t.d.CourierConfig().CourierTemplatesVerificationCodeInvalid(ctx).Body.PlainText,
68+
)
69+
}
70+
71+
func (t *VerificationCodeInvalid) MarshalJSON() ([]byte, error) {
72+
return json.Marshal(t.m)
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright © 2022 Ory Corp
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package email_test
5+
6+
import (
7+
"context"
8+
"testing"
9+
10+
"github.com/ory/kratos/courier"
11+
"github.com/ory/kratos/courier/template/email"
12+
"github.com/ory/kratos/courier/template/testhelpers"
13+
"github.com/ory/kratos/internal"
14+
)
15+
16+
func TestVerifyCodeInvalid(t *testing.T) {
17+
ctx, cancel := context.WithCancel(context.Background())
18+
t.Cleanup(cancel)
19+
20+
t.Run("test=with courier templates directory", func(t *testing.T) {
21+
_, reg := internal.NewFastRegistryWithMocks(t)
22+
tpl := email.NewVerificationCodeInvalid(reg, &email.VerificationCodeInvalidModel{})
23+
24+
testhelpers.TestRendered(t, ctx, tpl)
25+
})
26+
27+
t.Run("test=with remote resources", func(t *testing.T) {
28+
testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/verification_code/invalid", courier.TypeVerificationCodeInvalid)
29+
})
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright © 2022 Ory Corp
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package email
5+
6+
import (
7+
"context"
8+
"encoding/json"
9+
"os"
10+
"strings"
11+
12+
"github.com/ory/kratos/courier/template"
13+
)
14+
15+
type (
16+
VerificationCodeValid struct {
17+
d template.Dependencies
18+
m *VerificationCodeValidModel
19+
}
20+
VerificationCodeValidModel struct {
21+
To string
22+
VerificationURL string
23+
VerificationCode string
24+
Identity map[string]interface{}
25+
}
26+
)
27+
28+
func NewVerificationCodeValid(d template.Dependencies, m *VerificationCodeValidModel) *VerificationCodeValid {
29+
return &VerificationCodeValid{d: d, m: m}
30+
}
31+
32+
func (t *VerificationCodeValid) EmailRecipient() (string, error) {
33+
return t.m.To, nil
34+
}
35+
36+
func (t *VerificationCodeValid) EmailSubject(ctx context.Context) (string, error) {
37+
subject, err := template.LoadText(
38+
ctx,
39+
t.d,
40+
os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)),
41+
"verification_code/valid/email.subject.gotmpl",
42+
"verification_code/valid/email.subject*",
43+
t.m,
44+
t.d.CourierConfig().CourierTemplatesVerificationCodeValid(ctx).Subject,
45+
)
46+
47+
return strings.TrimSpace(subject), err
48+
}
49+
50+
func (t *VerificationCodeValid) EmailBody(ctx context.Context) (string, error) {
51+
return template.LoadHTML(ctx,
52+
t.d,
53+
os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)),
54+
"verification_code/valid/email.body.gotmpl",
55+
"verification_code/valid/email.body*",
56+
t.m,
57+
t.d.CourierConfig().CourierTemplatesVerificationCodeValid(ctx).Body.HTML,
58+
)
59+
}
60+
61+
func (t *VerificationCodeValid) EmailBodyPlaintext(ctx context.Context) (string, error) {
62+
return template.LoadText(ctx,
63+
t.d,
64+
os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)),
65+
"verification_code/valid/email.body.plaintext.gotmpl",
66+
"verification_code/valid/email.body.plaintext*",
67+
t.m,
68+
t.d.CourierConfig().CourierTemplatesVerificationCodeValid(ctx).Body.PlainText,
69+
)
70+
}
71+
72+
func (t *VerificationCodeValid) MarshalJSON() ([]byte, error) {
73+
return json.Marshal(t.m)
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright © 2022 Ory Corp
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package email_test
5+
6+
import (
7+
"context"
8+
"testing"
9+
10+
"github.com/ory/kratos/courier"
11+
"github.com/ory/kratos/courier/template/email"
12+
"github.com/ory/kratos/courier/template/testhelpers"
13+
"github.com/ory/kratos/internal"
14+
)
15+
16+
func TestVerifyCodeValid(t *testing.T) {
17+
ctx, cancel := context.WithCancel(context.Background())
18+
t.Cleanup(cancel)
19+
20+
t.Run("test=with courier templates directory", func(t *testing.T) {
21+
_, reg := internal.NewFastRegistryWithMocks(t)
22+
tpl := email.NewVerificationCodeValid(reg, &email.VerificationCodeValidModel{})
23+
24+
testhelpers.TestRendered(t, ctx, tpl)
25+
})
26+
27+
t.Run("test=with remote resources", func(t *testing.T) {
28+
testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/verification_code/valid", courier.TypeVerificationCodeValid)
29+
})
30+
}

0 commit comments

Comments
 (0)