diff --git a/CHANGELOG.md b/CHANGELOG.md index ead3d791c142..7d2c0f47a526 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ **Table of Contents** -- [ (2023-11-22)](#2023-11-22) +- [ (2024-01-08)](#2024-01-08) - [Breaking Changes](#breaking-changes) - [Bug Fixes](#bug-fixes) - [Documentation](#documentation) @@ -314,7 +314,7 @@ -# [](https://github.com/ory/kratos/compare/v1.0.0...v) (2023-11-22) +# [](https://github.com/ory/kratos/compare/v1.0.0...v) (2024-01-08) ## Breaking Changes @@ -420,6 +420,9 @@ https://github.com/ory/kratos/pull/3480 Signed-off-by: nxy7 +- Check whoami aal before accepting hydra login request + ([#3669](https://github.com/ory/kratos/issues/3669)) + ([a2f79c3](https://github.com/ory/kratos/commit/a2f79c31f3208b88024897fc8bf1307ccac6f895)) - Code method on registration and 2fa ([#3481](https://github.com/ory/kratos/issues/3481)) ([7aa2e29](https://github.com/ory/kratos/commit/7aa2e293175d0f4b6c13552cc3781f54f8caf3a0)) @@ -448,6 +451,14 @@ https://github.com/ory/kratos/pull/3480 - Don't return 500 on conflict for POST /admin/identities ([#3437](https://github.com/ory/kratos/issues/3437)) ([1429949](https://github.com/ory/kratos/commit/142994932e449d9948148804502c98ef73daafff)) +- Don't return nil if code is invalid + ([#3662](https://github.com/ory/kratos/issues/3662)) + ([df8ec2b](https://github.com/ory/kratos/commit/df8ec2b9b77a53beb32e3f94a8fccb711896d8e7)): + + - fix: don't return nil if code is invalid + + - chore: add test + - Error handling on identity import ([#3520](https://github.com/ory/kratos/issues/3520)) ([83bfb2d](https://github.com/ory/kratos/commit/83bfb2d2a9c69bf3a3442500b9484c1a69f8c794)): @@ -465,6 +476,8 @@ https://github.com/ory/kratos/pull/3480 Adds correct pagination parameters to the SDK methods for listing identities and sessions. +- Ignore CSRF middleware on Apple OIDC callback + ([309c506](https://github.com/ory/kratos/commit/309c50694c11162cad070337f9b1d4e0fcdf444b)) - Ignore more cloudflare cookies ([#3499](https://github.com/ory/kratos/issues/3499)) ([f124ab5](https://github.com/ory/kratos/commit/f124ab5586781cdbfc0a0cfd11b4355bfc8a115c)) @@ -473,12 +486,20 @@ https://github.com/ory/kratos/pull/3480 This also improves tracing in the OIDC strategy. +- Incorrect login accept challenge + ([#3658](https://github.com/ory/kratos/issues/3658)) + ([b5dede3](https://github.com/ory/kratos/commit/b5dede329247d0962688b15872a6caf027cf910f)) - Incorrect sdk generator path ([#3488](https://github.com/ory/kratos/issues/3488)) ([ed996c0](https://github.com/ory/kratos/commit/ed996c0d25e68e8a2c7de861c546f0b0e42e9e6e)) - Incorrect SMTP error handling ([#3636](https://github.com/ory/kratos/issues/3636)) ([ee138ec](https://github.com/ory/kratos/commit/ee138ec4e1ba55ef077858653220db9e6b0c7254)) +- Incorrect swagger spec for filter parameter + ([#3684](https://github.com/ory/kratos/issues/3684)) + ([2c1470a](https://github.com/ory/kratos/commit/2c1470ab3556e639f06a01ac1646a6b90c7ecac7)), + closes [#3676](https://github.com/ory/kratos/issues/3676) + [#3675](https://github.com/ory/kratos/issues/3675) - Increase connection-level timeouts and shutdown timeouts ([#3570](https://github.com/ory/kratos/issues/3570)) ([200b413](https://github.com/ory/kratos/commit/200b4138a429d113ee045d16031bb0a6312c1c01)): @@ -533,6 +554,8 @@ https://github.com/ory/kratos/pull/3480 - chore: refactor +- Panic in recovery ([#3639](https://github.com/ory/kratos/issues/3639)) + ([c25ddff](https://github.com/ory/kratos/commit/c25ddffd2270a8d0861e2fc78cd0ba26e63af4eb)) - Pass context ([#3452](https://github.com/ory/kratos/issues/3452)) ([c492bdc](https://github.com/ory/kratos/commit/c492bdcd0c5dbdf527ae523d879a6c1eeb9c4cdf)) - Properly normalize OIDC verified emails @@ -584,6 +607,8 @@ https://github.com/ory/kratos/pull/3480 - Registration with verification ([#3451](https://github.com/ory/kratos/issues/3451)) ([77c3196](https://github.com/ory/kratos/commit/77c3196fd60c5927b84e9a7f6546f80ac2d78ee5)) +- Reject obviously invalid email addresses from courier + ([8cb9e4c](https://github.com/ory/kratos/commit/8cb9e4cae9dffd4c25d52920186f9c5fbe2bd0fe)) - Remove `earliest_possible_extend` default in schema ([#3464](https://github.com/ory/kratos/issues/3464)) ([7e05b7d](https://github.com/ory/kratos/commit/7e05b7db3c01efc96185ac18042e971e33da37c8)) @@ -596,6 +621,9 @@ https://github.com/ory/kratos/pull/3480 - Remove slow queries from update identities ([#3553](https://github.com/ory/kratos/issues/3553)) ([d138abb](https://github.com/ory/kratos/commit/d138abb6278ebb232e120bee0fb956a0f2816b8d)) +- Rename "phone" courier channel to "sms" + ([#3680](https://github.com/ory/kratos/issues/3680)) + ([eb8d1b9](https://github.com/ory/kratos/commit/eb8d1b9abd6d2b3eb86ab11d48d9ebd059586b67)) - Respect gomail.SendError in mail queue ([#3600](https://github.com/ory/kratos/issues/3600)) ([9c608b9](https://github.com/ory/kratos/commit/9c608b991874d839782d9219f2fc27d0d4a398af)) @@ -646,9 +674,15 @@ https://github.com/ory/kratos/pull/3480 - test: update snapshot +- Use ID label on login with multiple identifiers + ([#3657](https://github.com/ory/kratos/issues/3657)) + ([be907db](https://github.com/ory/kratos/commit/be907dbbd841025fd854344b77d3368b2ff8089f)) - Use org ID from session if available in login flow ([#3545](https://github.com/ory/kratos/issues/3545)) ([1b3647c](https://github.com/ory/kratos/commit/1b3647c2acdad966f920c2b9e6e657c52aa50c6e)) +- Use provider label in link message + ([#3661](https://github.com/ory/kratos/issues/3661)) + ([fa5ec93](https://github.com/ory/kratos/commit/fa5ec93e8ae7d971d07f0e9b3acaa0840b9ac7de)) - Use registry client for schema loading ([#3471](https://github.com/ory/kratos/issues/3471)) ([3a57726](https://github.com/ory/kratos/commit/3a577269980213e4415fd5fa713882990e2e7640)) @@ -700,6 +734,9 @@ https://github.com/ory/kratos/pull/3480 - Add OpenTelemetry span for password hash comparison ([#3383](https://github.com/ory/kratos/issues/3383)) ([e3fcf0c](https://github.com/ory/kratos/commit/e3fcf0c31db9742ed61bcf783e37ee119ed19d42)) +- Add sms verification for phone numbers + ([#3649](https://github.com/ory/kratos/issues/3649)) + ([e3a3c4f](https://github.com/ory/kratos/commit/e3a3c4fe0d6697f6864283daf4be8a8f8971c7b4)) - Add support for recovery on native flows ([#3273](https://github.com/ory/kratos/issues/3273)) ([e363889](https://github.com/ory/kratos/commit/e363889732c0a1cb801fd12b2e0e8546006e9714)) @@ -840,6 +877,9 @@ https://github.com/ory/kratos/pull/3480 This feature depends on Cockroach functionality and configuration, and is not possible for MySQL or PostgreSQL. +- Extract identifier label for login from default identity schema + ([#3645](https://github.com/ory/kratos/issues/3645)) + ([180828e](https://github.com/ory/kratos/commit/180828eb507ab239a9c6589f747a6816b6e50074)) - Fine-grained hooks for all available flow methods ([#3519](https://github.com/ory/kratos/issues/3519)) ([a37f6bd](https://github.com/ory/kratos/commit/a37f6bddc48443b2fc464699fa5c2922f64d81f6)): diff --git a/Makefile b/Makefile index 59fd30158bdb..fd023e75bf13 100644 --- a/Makefile +++ b/Makefile @@ -125,6 +125,7 @@ sdk: .bin/swagger .bin/ory node_modules --git-user-id ory \ --git-repo-id client-go \ --git-host github.com \ + --api-name-suffix "Api" \ -t .schema/openapi/templates/go \ -c .schema/openapi/gen.go.yml @@ -138,6 +139,7 @@ sdk: .bin/swagger .bin/ory node_modules --git-user-id ory \ --git-repo-id client-go \ --git-host github.com \ + --api-name-suffix "Api" \ -t .schema/openapi/templates/go \ -c .schema/openapi/gen.go.yml diff --git a/contrib/quickstart/kratos/phone-password/identity.schema.json b/contrib/quickstart/kratos/phone-password/identity.schema.json new file mode 100644 index 000000000000..0d986aeb672e --- /dev/null +++ b/contrib/quickstart/kratos/phone-password/identity.schema.json @@ -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": "sms" + } + } + } + }, + "required": ["phone"], + "additionalProperties": false + } + } +} diff --git a/contrib/quickstart/kratos/phone-password/kratos.yml b/contrib/quickstart/kratos/phone-password/kratos.yml new file mode 100644 index 000000000000..88ee01bc84e6 --- /dev/null +++ b/contrib/quickstart/kratos/phone-password/kratos.yml @@ -0,0 +1,114 @@ +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: sms + type: http + request_config: + url: https://api.twilio.com/2010-04-01/Accounts/AXXXXXXXXXXXXXX/Messages.json + method: POST + body: base64://ZnVuY3Rpb24oY3R4KSB7ClRvOiBjdHguUmVjaXBpZW50LApCb2R5OiBjdHguQm9keSwKfQ== + headers: + Content-Type: application/x-www-form-urlencoded + auth: + type: basic_auth + config: + user: AXXXXXXX + password: XXXX + +feature_flags: + use_continue_with_transitions: true diff --git a/courier/channel.go b/courier/channel.go new file mode 100644 index 000000000000..12697a67de3b --- /dev/null +++ b/courier/channel.go @@ -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 +} diff --git a/courier/courier.go b/courier/courier.go index b42a580fc061..231b4007060f 100644 --- a/courier/courier.go +++ b/courier/courier.go @@ -7,16 +7,15 @@ import ( "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 ( @@ -33,11 +32,8 @@ type ( 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() } @@ -51,9 +47,7 @@ type ( } courier struct { - smsClient *smsClient - smtpClient *smtpClient - httpClient *httpClient + courierChannels map[string]Channel deps Dependencies failOnDispatchError bool backoff backoff.BackOff @@ -61,16 +55,34 @@ type ( ) 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 + } + 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) + } + } + 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 } diff --git a/courier/courier_dispatcher.go b/courier/courier_dispatcher.go index 8470c024fca4..3d4835636206 100644 --- a/courier/courier_dispatcher.go +++ b/courier/courier_dispatcher.go @@ -19,17 +19,13 @@ 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 { @@ -37,6 +33,7 @@ func (c *courier) DispatchMessage(ctx context.Context, msg Message) error { 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 } @@ -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 diff --git a/courier/courier_dispatcher_test.go b/courier/courier_dispatcher_test.go index 528badf2de02..1f8c94ddf3e2 100644 --- a/courier/courier_dispatcher_test.go +++ b/courier/courier_dispatcher_test.go @@ -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 { @@ -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() diff --git a/courier/email_templates_test.go b/courier/email_templates_test.go deleted file mode 100644 index 40afb5dc6863..000000000000 --- a/courier/email_templates_test.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright © 2023 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - -package courier_test - -import ( - "context" - "encoding/json" - "fmt" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/ory/kratos/courier" - "github.com/ory/kratos/courier/template/email" - "github.com/ory/kratos/internal" -) - -func TestGetTemplateType(t *testing.T) { - for expectedType, tmpl := range map[courier.TemplateType]courier.EmailTemplate{ - courier.TypeRecoveryInvalid: &email.RecoveryInvalid{}, - courier.TypeRecoveryValid: &email.RecoveryValid{}, - courier.TypeRecoveryCodeInvalid: &email.RecoveryCodeInvalid{}, - courier.TypeRecoveryCodeValid: &email.RecoveryCodeValid{}, - courier.TypeVerificationInvalid: &email.VerificationInvalid{}, - courier.TypeVerificationValid: &email.VerificationValid{}, - courier.TypeVerificationCodeInvalid: &email.VerificationCodeInvalid{}, - courier.TypeVerificationCodeValid: &email.VerificationCodeValid{}, - courier.TypeTestStub: &email.TestStub{}, - courier.TypeLoginCodeValid: &email.LoginCodeValid{}, - courier.TypeRegistrationCodeValid: &email.RegistrationCodeValid{}, - } { - t.Run(fmt.Sprintf("case=%s", expectedType), func(t *testing.T) { - actualType, err := courier.GetEmailTemplateType(tmpl) - require.NoError(t, err) - require.Equal(t, expectedType, actualType) - }) - } -} - -func TestNewEmailTemplateFromMessage(t *testing.T) { - _, reg := internal.NewFastRegistryWithMocks(t) - ctx := context.Background() - - for tmplType, expectedTmpl := range map[courier.TemplateType]courier.EmailTemplate{ - courier.TypeRecoveryInvalid: email.NewRecoveryInvalid(reg, &email.RecoveryInvalidModel{To: "foo"}), - courier.TypeRecoveryValid: email.NewRecoveryValid(reg, &email.RecoveryValidModel{To: "bar", RecoveryURL: "http://foo.bar"}), - courier.TypeRecoveryCodeValid: email.NewRecoveryCodeValid(reg, &email.RecoveryCodeValidModel{To: "bar", RecoveryCode: "12345678"}), - courier.TypeRecoveryCodeInvalid: email.NewRecoveryCodeInvalid(reg, &email.RecoveryCodeInvalidModel{To: "bar"}), - courier.TypeVerificationInvalid: email.NewVerificationInvalid(reg, &email.VerificationInvalidModel{To: "baz"}), - courier.TypeVerificationValid: email.NewVerificationValid(reg, &email.VerificationValidModel{To: "faz", VerificationURL: "http://bar.foo"}), - courier.TypeVerificationCodeInvalid: email.NewVerificationCodeInvalid(reg, &email.VerificationCodeInvalidModel{To: "baz"}), - courier.TypeVerificationCodeValid: email.NewVerificationCodeValid(reg, &email.VerificationCodeValidModel{To: "faz", VerificationURL: "http://bar.foo", VerificationCode: "123456678"}), - courier.TypeTestStub: email.NewTestStub(reg, &email.TestStubModel{To: "far", Subject: "test subject", Body: "test body"}), - courier.TypeLoginCodeValid: email.NewLoginCodeValid(reg, &email.LoginCodeValidModel{To: "far", LoginCode: "123456"}), - courier.TypeRegistrationCodeValid: email.NewRegistrationCodeValid(reg, &email.RegistrationCodeValidModel{To: "far", RegistrationCode: "123456"}), - } { - t.Run(fmt.Sprintf("case=%s", tmplType), func(t *testing.T) { - tmplData, err := json.Marshal(expectedTmpl) - require.NoError(t, err) - - m := courier.Message{TemplateType: tmplType, TemplateData: tmplData} - actualTmpl, err := courier.NewEmailTemplateFromMessage(reg, m) - require.NoError(t, err) - - require.IsType(t, expectedTmpl, actualTmpl) - - expectedRecipient, err := expectedTmpl.EmailRecipient() - require.NoError(t, err) - actualRecipient, err := actualTmpl.EmailRecipient() - require.NoError(t, err) - require.Equal(t, expectedRecipient, actualRecipient) - - expectedSubject, err := expectedTmpl.EmailSubject(ctx) - require.NoError(t, err) - actualSubject, err := actualTmpl.EmailSubject(ctx) - require.NoError(t, err) - require.Equal(t, expectedSubject, actualSubject) - - expectedBody, err := expectedTmpl.EmailBody(ctx) - require.NoError(t, err) - actualBody, err := actualTmpl.EmailBody(ctx) - require.NoError(t, err) - require.Equal(t, expectedBody, actualBody) - - expectedBodyPlaintext, err := expectedTmpl.EmailBodyPlaintext(ctx) - require.NoError(t, err) - actualBodyPlaintext, err := actualTmpl.EmailBodyPlaintext(ctx) - require.NoError(t, err) - require.Equal(t, expectedBodyPlaintext, actualBodyPlaintext) - }) - } -} diff --git a/courier/http.go b/courier/http.go deleted file mode 100644 index 6542d26964af..000000000000 --- a/courier/http.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright © 2023 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - -package courier - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/ory/kratos/request" - "github.com/ory/x/otelx" -) - -type httpDataModel struct { - Recipient string - Subject string - Body string - TemplateType TemplateType - TemplateData EmailTemplate -} - -type httpClient struct { - RequestConfig json.RawMessage -} - -func newHTTP(ctx context.Context, deps Dependencies) *httpClient { - return &httpClient{ - RequestConfig: deps.CourierConfig().CourierEmailRequestConfig(ctx), - } -} -func (c *courier) dispatchMailerEmail(ctx context.Context, msg Message) (err error) { - ctx, span := c.deps.Tracer(ctx).Tracer().Start(ctx, "courier.http.dispatchMailerEmail") - defer otelx.End(span, &err) - - builder, err := request.NewBuilder(ctx, c.httpClient.RequestConfig, c.deps) - if err != nil { - return err - } - - tmpl, err := c.smtpClient.NewTemplateFromMessage(c.deps, msg) - if err != nil { - return err - } - - td := httpDataModel{ - Recipient: msg.Recipient, - Subject: msg.Subject, - Body: msg.Body, - TemplateType: msg.TemplateType, - TemplateData: tmpl, - } - - req, err := builder.BuildRequest(ctx, td) - if err != nil { - return err - } - - res, err := c.deps.HTTPClient(ctx).Do(req) - if err != nil { - return err - } - - defer res.Body.Close() - - if res.StatusCode >= 200 && res.StatusCode < 300 { - c.deps.Logger(). - WithField("message_id", msg.ID). - WithField("message_type", msg.Type). - WithField("message_template_type", msg.TemplateType). - WithField("message_subject", msg.Subject). - Debug("Courier sent out mailer.") - return nil - } - - err = fmt.Errorf( - "unable to dispatch mail delivery because upstream server replied with status code %d", - res.StatusCode, - ) - c.deps.Logger(). - WithField("message_id", msg.ID). - WithField("message_type", msg.Type). - WithField("message_template_type", msg.TemplateType). - WithField("message_subject", msg.Subject). - WithError(err). - Error("sending mail via HTTP failed.") - return err -} diff --git a/courier/http_channel.go b/courier/http_channel.go new file mode 100644 index 000000000000..41315f33e067 --- /dev/null +++ b/courier/http_channel.go @@ -0,0 +1,124 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/pkg/errors" + + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/request" + "github.com/ory/kratos/x" + "github.com/ory/x/jsonnetsecure" + "github.com/ory/x/otelx" +) + +type ( + httpChannel struct { + id string + requestConfig json.RawMessage + d channelDependencies + } + channelDependencies interface { + x.TracingProvider + x.LoggingProvider + x.HTTPClientProvider + jsonnetsecure.VMProvider + ConfigProvider + } +) + +var _ Channel = new(httpChannel) + +func newHttpChannel(id string, requestConfig json.RawMessage, d channelDependencies) *httpChannel { + return &httpChannel{ + id: id, + requestConfig: requestConfig, + d: d, + } +} + +func (c *httpChannel) ID() string { + return c.id +} + +type httpDataModel struct { + Recipient string + Subject string + Body string + TemplateType template.TemplateType + TemplateData Template + MessageType string +} + +func (c *httpChannel) Dispatch(ctx context.Context, msg Message) (err error) { + ctx, span := c.d.Tracer(ctx).Tracer().Start(ctx, "courier.httpChannel.Dispatch") + defer otelx.End(span, &err) + + builder, err := request.NewBuilder(ctx, c.requestConfig, c.d) + if err != nil { + return errors.WithStack(err) + } + + tmpl, err := newTemplate(c.d, msg) + if err != nil { + return errors.WithStack(err) + } + + td := httpDataModel{ + Recipient: msg.Recipient, + Subject: msg.Subject, + Body: msg.Body, + TemplateType: msg.TemplateType, + TemplateData: tmpl, + MessageType: msg.Type.String(), + } + + req, err := builder.BuildRequest(ctx, td) + if err != nil { + return errors.WithStack(err) + } + + res, err := c.d.HTTPClient(ctx).Do(req) + if err != nil { + return errors.WithStack(err) + } + + if res.StatusCode >= 200 && res.StatusCode < 300 { + c.d.Logger(). + WithField("message_id", msg.ID). + WithField("message_type", msg.Type). + WithField("message_template_type", msg.TemplateType). + WithField("message_subject", msg.Subject). + Debug("Courier sent out mailer.") + return nil + } + + err = errors.Errorf( + "unable to dispatch mail delivery because upstream server replied with status code %d", + res.StatusCode, + ) + c.d.Logger(). + WithField("message_id", msg.ID). + WithField("message_type", msg.Type). + WithField("message_template_type", msg.TemplateType). + WithField("message_subject", msg.Subject). + WithError(err). + Error("sending mail via HTTP failed.") + return errors.WithStack(err) +} + +func newTemplate(d template.Dependencies, msg Message) (Template, error) { + switch msg.Type { + case MessageTypeEmail: + return NewEmailTemplateFromMessage(d, msg) + case MessageTypeSMS: + return NewSMSTemplateFromMessage(d, msg) + default: + return nil, fmt.Errorf("received unexpected message type: %s", msg.Type) + } +} diff --git a/courier/message.go b/courier/message.go index 9ba9cf9cdb6c..ef39514aee93 100644 --- a/courier/message.go +++ b/courier/message.go @@ -12,7 +12,9 @@ import ( "github.com/pkg/errors" "github.com/ory/herodot" + "github.com/ory/kratos/courier/template" "github.com/ory/x/pagination/keysetpagination" + "github.com/ory/x/sqlxx" "github.com/ory/x/stringsx" ) @@ -88,7 +90,6 @@ func (ms *MessageStatus) UnmarshalJSON(data []byte) error { } s, err := ToMessageStatus(str) - if err != nil { return err } @@ -106,12 +107,12 @@ type MessageType int const ( MessageTypeEmail MessageType = iota + 1 - MessageTypePhone + MessageTypeSMS ) const ( messageTypeEmailText = "email" - messageTypePhoneText = "phone" + messageTypeSMSText = "sms" ) // The format we need to use in the Page tokens, as it's the only format that is understood by all DBs @@ -121,8 +122,8 @@ func ToMessageType(str string) (MessageType, error) { switch s := stringsx.SwitchExact(str); { case s.AddCase(messageTypeEmailText): return MessageTypeEmail, nil - case s.AddCase(messageTypePhoneText): - return MessageTypePhone, nil + case s.AddCase(messageTypeSMSText): + return MessageTypeSMS, nil default: return 0, errors.WithStack(herodot.ErrBadRequest.WithWrap(s.ToUnknownCaseErr()).WithReason("Message type is not valid")) } @@ -132,8 +133,8 @@ func (mt MessageType) String() string { switch mt { case MessageTypeEmail: return messageTypeEmailText - case MessageTypePhone: - return messageTypePhoneText + case MessageTypeSMS: + return messageTypeSMSText default: return "" } @@ -141,7 +142,7 @@ func (mt MessageType) String() string { func (mt MessageType) IsValid() error { switch mt { - case MessageTypeEmail, MessageTypePhone: + case MessageTypeEmail, MessageTypeSMS: return nil default: return errors.WithStack(herodot.ErrBadRequest.WithReason("Message type is not valid")) @@ -187,7 +188,9 @@ type Message struct { // required: true Subject string `json:"subject" db:"subject"` // required: true - TemplateType TemplateType `json:"template_type" db:"template_type"` + TemplateType template.TemplateType `json:"template_type" db:"template_type"` + + Channel sqlxx.NullString `json:"channel" db:"channel"` TemplateData []byte `json:"-" db:"template_data"` // required: true diff --git a/courier/message_test.go b/courier/message_test.go index e8db6713bcf5..c359f19e67a8 100644 --- a/courier/message_test.go +++ b/courier/message_test.go @@ -46,7 +46,7 @@ func TestToMessageType(t *testing.T) { t.Run("case=should return corresponding MessageType for given str", func(t *testing.T) { for str, exp := range map[string]courier.MessageType{ "email": courier.MessageTypeEmail, - "phone": courier.MessageTypePhone, + "sms": courier.MessageTypeSMS, } { result, err := courier.ToMessageType(str) require.NoError(t, err) diff --git a/courier/sms.go b/courier/sms.go index e6b7a1925a9b..122f8a5f1ed1 100644 --- a/courier/sms.go +++ b/courier/sms.go @@ -6,60 +6,34 @@ package courier import ( "context" "encoding/json" - "net/http" - - "github.com/pkg/errors" - - "github.com/ory/herodot" "github.com/gofrs/uuid" - - "github.com/ory/kratos/request" ) -type sendSMSRequestBody struct { - From string `json:"from"` - To string `json:"to"` - Body string `json:"body"` -} - -type smsClient struct { - RequestConfig json.RawMessage - - GetTemplateType func(t SMSTemplate) (TemplateType, error) - NewTemplateFromMessage func(d Dependencies, msg Message) (SMSTemplate, error) -} - -func newSMS(ctx context.Context, deps Dependencies) *smsClient { - return &smsClient{ - RequestConfig: deps.CourierConfig().CourierSMSRequestConfig(ctx), - GetTemplateType: SMSTemplateType, - NewTemplateFromMessage: NewSMSTemplateFromMessage, - } -} - func (c *courier) QueueSMS(ctx context.Context, t SMSTemplate) (uuid.UUID, error) { recipient, err := t.PhoneNumber() if err != nil { return uuid.Nil, err } - templateType, err := c.smsClient.GetTemplateType(t) + templateData, err := json.Marshal(t) if err != nil { return uuid.Nil, err } - templateData, err := json.Marshal(t) + body, err := t.SMSBody(ctx) if err != nil { return uuid.Nil, err } message := &Message{ Status: MessageStatusQueued, - Type: MessageTypePhone, + Type: MessageTypeSMS, + Channel: "sms", Recipient: recipient, - TemplateType: templateType, + TemplateType: t.TemplateType(), TemplateData: templateData, + Body: body, } if err := c.deps.CourierPersister().AddMessage(ctx, message); err != nil { return uuid.Nil, err @@ -67,49 +41,3 @@ func (c *courier) QueueSMS(ctx context.Context, t SMSTemplate) (uuid.UUID, error return message.ID, nil } - -func (c *courier) dispatchSMS(ctx context.Context, msg Message) error { - if !c.deps.CourierConfig().CourierSMSEnabled(ctx) { - return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Courier tried to deliver an sms but courier.sms.enabled is set to false!")) - } - - tmpl, err := c.smsClient.NewTemplateFromMessage(c.deps, msg) - if err != nil { - return err - } - - body, err := tmpl.SMSBody(ctx) - if err != nil { - return err - } - - builder, err := request.NewBuilder(ctx, c.smsClient.RequestConfig, c.deps) - if err != nil { - return err - } - - req, err := builder.BuildRequest(ctx, &sendSMSRequestBody{ - To: msg.Recipient, - From: c.deps.CourierConfig().CourierSMSFrom(ctx), - Body: body, - }) - if err != nil { - return err - } - - res, err := c.deps.HTTPClient(ctx).Do(req) - if err != nil { - return err - } - - defer res.Body.Close() - - switch res.StatusCode { - case http.StatusOK: - case http.StatusCreated: - default: - return errors.New(http.StatusText(res.StatusCode)) - } - - return nil -} diff --git a/courier/sms_templates.go b/courier/sms_templates.go index 0b1c58e3a635..683ba7d98ca3 100644 --- a/courier/sms_templates.go +++ b/courier/sms_templates.go @@ -9,6 +9,7 @@ import ( "github.com/pkg/errors" + "github.com/ory/kratos/courier/template" "github.com/ory/kratos/courier/template/sms" ) @@ -16,33 +17,24 @@ type SMSTemplate interface { json.Marshaler SMSBody(context.Context) (string, error) PhoneNumber() (string, error) + TemplateType() template.TemplateType } -func SMSTemplateType(t SMSTemplate) (TemplateType, error) { - switch t.(type) { - case *sms.OTPMessage: - return TypeOTP, nil - case *sms.TestStub: - return TypeTestStub, nil - default: - return "", errors.Errorf("unexpected template type") - } -} - -func NewSMSTemplateFromMessage(d Dependencies, m Message) (SMSTemplate, error) { +func NewSMSTemplateFromMessage(d template.Dependencies, m Message) (SMSTemplate, error) { switch m.TemplateType { - case TypeOTP: - var t sms.OTPMessageModel + case template.TypeVerificationCodeValid: + var t sms.VerificationCodeValidModel if err := json.Unmarshal(m.TemplateData, &t); err != nil { return nil, err } - return sms.NewOTPMessage(d, &t), nil - case TypeTestStub: + return sms.NewVerificationCodeValid(d, &t), nil + case template.TypeTestStub: var t sms.TestStubModel if err := json.Unmarshal(m.TemplateData, &t); err != nil { return nil, err } return sms.NewTestStub(d, &t), nil + default: return nil, errors.Errorf("received unexpected message template type: %s", m.TemplateType) } diff --git a/courier/sms_templates_test.go b/courier/sms_templates_test.go index 8d87e11f8956..2577b38eaad9 100644 --- a/courier/sms_templates_test.go +++ b/courier/sms_templates_test.go @@ -12,19 +12,18 @@ import ( "github.com/stretchr/testify/require" "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template" "github.com/ory/kratos/courier/template/sms" "github.com/ory/kratos/internal" ) func TestSMSTemplateType(t *testing.T) { - for expectedType, tmpl := range map[courier.TemplateType]courier.SMSTemplate{ - courier.TypeOTP: &sms.OTPMessage{}, - courier.TypeTestStub: &sms.TestStub{}, + for expectedType, tmpl := range map[template.TemplateType]courier.SMSTemplate{ + template.TypeVerificationCodeValid: &sms.VerificationCodeValid{}, + template.TypeTestStub: &sms.TestStub{}, } { t.Run(fmt.Sprintf("case=%s", expectedType), func(t *testing.T) { - actualType, err := courier.SMSTemplateType(tmpl) - require.NoError(t, err) - require.Equal(t, expectedType, actualType) + require.Equal(t, expectedType, tmpl.TemplateType()) }) } } @@ -33,9 +32,9 @@ func TestNewSMSTemplateFromMessage(t *testing.T) { _, reg := internal.NewFastRegistryWithMocks(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"}), + for tmplType, expectedTmpl := range map[template.TemplateType]courier.SMSTemplate{ + template.TypeVerificationCodeValid: sms.NewVerificationCodeValid(reg, &sms.VerificationCodeValidModel{To: "+12345678901"}), + template.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) diff --git a/courier/sms_test.go b/courier/sms_test.go index 5a919788fe04..a93a7974bf71 100644 --- a/courier/sms_test.go +++ b/courier/sms_test.go @@ -14,7 +14,6 @@ import ( "time" "github.com/gofrs/uuid" - "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -22,7 +21,6 @@ import ( "github.com/ory/kratos/courier/template/sms" "github.com/ory/kratos/driver/config" "github.com/ory/kratos/internal" - "github.com/ory/x/resilience" ) func TestQueueSMS(t *testing.T) { @@ -80,9 +78,13 @@ func TestQueueSMS(t *testing.T) { }`, srv.URL) conf, reg := internal.NewFastRegistryWithMocks(t) - conf.MustSet(ctx, config.ViperKeyCourierSMSRequestConfig, requestConfig) - conf.MustSet(ctx, config.ViperKeyCourierSMSFrom, expectedSender) - conf.MustSet(ctx, config.ViperKeyCourierSMSEnabled, true) + conf.MustSet(ctx, config.ViperKeyCourierChannels, fmt.Sprintf(`[ + { + "id": "sms", + "type": "http", + "request_config": %s + } + ]`, requestConfig)) conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "http://foo.url") reg.Logger().Level = logrus.TraceLevel @@ -98,16 +100,11 @@ func TestQueueSMS(t *testing.T) { require.NotEqual(t, uuid.Nil, id) } - go func() { - require.NoError(t, c.Work(ctx)) - }() + require.NoError(t, c.DispatchQueue(ctx)) - require.NoError(t, resilience.Retry(reg.Logger(), time.Millisecond*250, time.Second*10, func() error { - if len(actual) == len(expectedSMS) { - return nil - } - return errors.New("capacity not reached") - })) + require.Eventually(t, func() bool { + return len(actual) == len(expectedSMS) + }, 10*time.Second, 250*time.Millisecond) for i, message := range actual { expected := expectedSMS[i] @@ -123,15 +120,19 @@ func TestDisallowedInternalNetwork(t *testing.T) { ctx := context.Background() conf, reg := internal.NewFastRegistryWithMocks(t) - conf.MustSet(ctx, config.ViperKeyCourierSMSRequestConfig, `{ - "url": "http://127.0.0.1/", - "method": "GET", - "body": "file://./stub/request.config.twilio.jsonnet" - }`) - conf.MustSet(ctx, config.ViperKeyCourierSMSEnabled, true) + conf.MustSet(ctx, config.ViperKeyCourierChannels, `[ + { + "id": "sms", + "type": "http", + "request_config": { + "url": "http://127.0.0.1/", + "method": "GET", + "body": "file://./stub/request.config.twilio.jsonnet" + } + } + ]`) conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "http://foo.url") conf.MustSet(ctx, config.ViperKeyClientHTTPNoPrivateIPRanges, true) - reg.Logger().Level = logrus.TraceLevel c, err := reg.Courier(ctx) require.NoError(t, err) diff --git a/courier/smtp.go b/courier/smtp.go index 3c60b3361594..8c7ef91fbe33 100644 --- a/courier/smtp.go +++ b/courier/smtp.go @@ -7,41 +7,33 @@ import ( "context" "crypto/tls" "encoding/json" - "fmt" - "net/textproto" + "net/mail" + "net/url" "strconv" "time" - "github.com/ory/kratos/courier/template" - + "github.com/ory/herodot" "github.com/ory/kratos/driver/config" "github.com/gofrs/uuid" - "github.com/pkg/errors" - "github.com/ory/herodot" gomail "github.com/ory/mail/v3" ) -type smtpClient struct { +type SMTPClient struct { *gomail.Dialer - - GetTemplateType func(t EmailTemplate) (TemplateType, error) - NewTemplateFromMessage func(d template.Dependencies, msg Message) (EmailTemplate, error) } -func newSMTP(ctx context.Context, deps Dependencies) (*smtpClient, error) { - uri, err := deps.CourierConfig().CourierSMTPURL(ctx) +func NewSMTPClient(deps Dependencies, cfg *config.SMTPConfig) (*SMTPClient, error) { + uri, err := url.Parse(cfg.ConnectionURI) if err != nil { - return nil, err + return nil, herodot.ErrInternalServerError.WithError(err.Error()) } var tlsCertificates []tls.Certificate - clientCertPath := deps.CourierConfig().CourierSMTPClientCertPath(ctx) - clientKeyPath := deps.CourierConfig().CourierSMTPClientKeyPath(ctx) - if clientCertPath != "" && clientKeyPath != "" { - clientCert, err := tls.LoadX509KeyPair(clientCertPath, clientKeyPath) + if cfg.ClientCertPath != "" && cfg.ClientKeyPath != "" { + clientCert, err := tls.LoadX509KeyPair(cfg.ClientCertPath, cfg.ClientKeyPath) if err == nil { tlsCertificates = append(tlsCertificates, clientCert) } else { @@ -51,7 +43,6 @@ func newSMTP(ctx context.Context, deps Dependencies) (*smtpClient, error) { } } - localName := deps.CourierConfig().CourierSMTPLocalName(ctx) password, _ := uri.User.Password() port, _ := strconv.ParseInt(uri.Port(), 10, 0) @@ -60,7 +51,7 @@ func newSMTP(ctx context.Context, deps Dependencies) (*smtpClient, error) { Port: int(port), Username: uri.User.Username(), Password: password, - LocalName: localName, + LocalName: cfg.LocalName, Timeout: time.Second * 10, RetryFailure: true, @@ -93,43 +84,26 @@ func newSMTP(ctx context.Context, deps Dependencies) (*smtpClient, error) { dialer.SSL = true } - return &smtpClient{ + return &SMTPClient{ Dialer: dialer, - - GetTemplateType: GetEmailTemplateType, - NewTemplateFromMessage: NewEmailTemplateFromMessage, }, nil } -func (c *courier) SetGetEmailTemplateType(f func(t EmailTemplate) (TemplateType, error)) { - c.smtpClient.GetTemplateType = f -} - -func (c *courier) SetNewEmailTemplateFromMessage(f func(d template.Dependencies, msg Message) (EmailTemplate, error)) { - c.smtpClient.NewTemplateFromMessage = f -} - -func (c *courier) SmtpDialer() *gomail.Dialer { - return c.smtpClient.Dialer -} - func (c *courier) QueueEmail(ctx context.Context, t EmailTemplate) (uuid.UUID, error) { recipient, err := t.EmailRecipient() if err != nil { return uuid.Nil, err } - - subject, err := t.EmailSubject(ctx) - if err != nil { + if _, err := mail.ParseAddress(recipient); err != nil { return uuid.Nil, err } - bodyPlaintext, err := t.EmailBodyPlaintext(ctx) + subject, err := t.EmailSubject(ctx) if err != nil { return uuid.Nil, err } - templateType, err := c.smtpClient.GetTemplateType(t) + bodyPlaintext, err := t.EmailBodyPlaintext(ctx) if err != nil { return uuid.Nil, err } @@ -142,10 +116,11 @@ func (c *courier) QueueEmail(ctx context.Context, t EmailTemplate) (uuid.UUID, e message := &Message{ Status: MessageStatusQueued, Type: MessageTypeEmail, + Channel: "email", Recipient: recipient, Body: bodyPlaintext, Subject: subject, - TemplateType: templateType, + TemplateType: t.TemplateType(), TemplateData: templateData, } @@ -155,95 +130,3 @@ func (c *courier) QueueEmail(ctx context.Context, t EmailTemplate) (uuid.UUID, e return message.ID, nil } - -func (c *courier) dispatchEmail(ctx context.Context, msg Message) error { - if c.deps.CourierConfig().CourierEmailStrategy(ctx) == "http" { - return c.dispatchMailerEmail(ctx, msg) - } - if c.smtpClient.Host == "" { - return errors.WithStack(herodot.ErrInternalServerError.WithErrorf("Courier tried to deliver an email but %s is not set!", config.ViperKeyCourierSMTPURL)) - } - - from := c.deps.CourierConfig().CourierSMTPFrom(ctx) - fromName := c.deps.CourierConfig().CourierSMTPFromName(ctx) - - gm := gomail.NewMessage() - if fromName == "" { - gm.SetHeader("From", from) - } else { - gm.SetAddressHeader("From", from, fromName) - } - - gm.SetHeader("To", msg.Recipient) - gm.SetHeader("Subject", msg.Subject) - - headers := c.deps.CourierConfig().CourierSMTPHeaders(ctx) - for k, v := range headers { - gm.SetHeader(k, v) - } - - gm.SetBody("text/plain", msg.Body) - - tmpl, err := c.smtpClient.NewTemplateFromMessage(c.deps, msg) - if err != nil { - c.deps.Logger(). - WithError(err). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). - Error(`Unable to get email template from message.`) - } else { - htmlBody, err := tmpl.EmailBody(ctx) - if err != nil { - c.deps.Logger(). - WithError(err). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). - Error(`Unable to get email body from template.`) - } else { - gm.AddAlternative("text/html", htmlBody) - } - } - - if err := c.smtpClient.DialAndSend(ctx, gm); err != nil { - c.deps.Logger(). - WithError(err). - WithField("smtp_server", fmt.Sprintf("%s:%d", c.smtpClient.Host, c.smtpClient.Port)). - WithField("smtp_ssl_enabled", c.smtpClient.SSL). - WithField("message_from", from). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). - Error("Unable to send email using SMTP connection.") - - var protoErr *textproto.Error - var mailErr *gomail.SendError - - switch { - case errors.As(err, &mailErr) && errors.As(mailErr.Cause, &protoErr) && protoErr.Code >= 500: - fallthrough - case errors.As(err, &protoErr) && protoErr.Code >= 500: - // See https://en.wikipedia.org/wiki/List_of_SMTP_server_return_codes - // If the SMTP server responds with 5xx, sending the message should not be retried (without changing something about the request) - if err := c.deps.CourierPersister().SetMessageStatus(ctx, msg.ID, MessageStatusAbandoned); err != nil { - c.deps.Logger(). - WithError(err). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). - Error(`Unable to reset the retried message's status to "abandoned".`) - return err - } - } - - return errors.WithStack(herodot.ErrInternalServerError. - WithError(err.Error()).WithReason("failed to send email via smtp")) - } - - c.deps.Logger(). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). - WithField("message_type", msg.Type). - WithField("message_template_type", msg.TemplateType). - WithField("message_subject", msg.Subject). - Debug("Courier sent out message.") - - return nil -} diff --git a/courier/smtp_channel.go b/courier/smtp_channel.go new file mode 100644 index 000000000000..a44719a351d6 --- /dev/null +++ b/courier/smtp_channel.go @@ -0,0 +1,144 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier + +import ( + "context" + "fmt" + "net/textproto" + + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/driver/config" + "github.com/ory/mail/v3" +) + +type ( + SMTPChannel struct { + smtpClient *SMTPClient + d Dependencies + + newEmailTemplateFromMessage func(d template.Dependencies, msg Message) (EmailTemplate, error) + } +) + +var _ Channel = new(SMTPChannel) + +func NewSMTPChannel(deps Dependencies, cfg *config.SMTPConfig) (*SMTPChannel, error) { + return NewSMTPChannelWithCustomTemplates(deps, cfg, NewEmailTemplateFromMessage) +} + +func NewSMTPChannelWithCustomTemplates(deps Dependencies, cfg *config.SMTPConfig, newEmailTemplateFromMessage func(d template.Dependencies, msg Message) (EmailTemplate, error)) (*SMTPChannel, error) { + smtpClient, err := NewSMTPClient(deps, cfg) + if err != nil { + return nil, err + } + return &SMTPChannel{ + smtpClient: smtpClient, + d: deps, + newEmailTemplateFromMessage: newEmailTemplateFromMessage, + }, nil +} + +func (c *SMTPChannel) ID() string { + return "email" +} + +func (c *SMTPChannel) Dispatch(ctx context.Context, msg Message) error { + if c.smtpClient.Host == "" { + return errors.WithStack(herodot.ErrInternalServerError.WithErrorf("Courier tried to deliver an email but %s is not set!", config.ViperKeyCourierSMTPURL)) + } + + channels, err := c.d.CourierConfig().CourierChannels(ctx) + if err != nil { + return err + } + + var cfg *config.SMTPConfig + for _, channel := range channels { + if channel.ID == "email" && channel.SMTPConfig != nil { + cfg = channel.SMTPConfig + break + } + } + + gm := mail.NewMessage() + if cfg.FromName == "" { + gm.SetHeader("From", cfg.FromAddress) + } else { + gm.SetAddressHeader("From", cfg.FromAddress, cfg.FromName) + } + + gm.SetHeader("To", msg.Recipient) + gm.SetHeader("Subject", msg.Subject) + + headers := cfg.Headers + for k, v := range headers { + gm.SetHeader(k, v) + } + + gm.SetBody("text/plain", msg.Body) + + tmpl, err := c.newEmailTemplateFromMessage(c.d, msg) + if err != nil { + c.d.Logger(). + WithError(err). + WithField("message_id", msg.ID). + WithField("message_nid", msg.NID). + Error(`Unable to get email template from message.`) + } else if htmlBody, err := tmpl.EmailBody(ctx); err != nil { + c.d.Logger(). + WithError(err). + WithField("message_id", msg.ID). + WithField("message_nid", msg.NID). + Error(`Unable to get email body from template.`) + } else { + gm.AddAlternative("text/html", htmlBody) + } + + if err := c.smtpClient.DialAndSend(ctx, gm); err != nil { + c.d.Logger(). + WithError(err). + WithField("smtp_server", fmt.Sprintf("%s:%d", c.smtpClient.Host, c.smtpClient.Port)). + WithField("smtp_ssl_enabled", c.smtpClient.SSL). + WithField("message_from", cfg.FromAddress). + WithField("message_id", msg.ID). + WithField("message_nid", msg.NID). + Error("Unable to send email using SMTP connection.") + + var protoErr *textproto.Error + var mailErr *mail.SendError + + switch { + case errors.As(err, &mailErr) && errors.As(mailErr.Cause, &protoErr) && protoErr.Code >= 500: + fallthrough + case errors.As(err, &protoErr) && protoErr.Code >= 500: + // See https://en.wikipedia.org/wiki/List_of_SMTP_server_return_codes + // If the SMTP server responds with 5xx, sending the message should not be retried (without changing something about the request) + if err := c.d.CourierPersister().SetMessageStatus(ctx, msg.ID, MessageStatusAbandoned); err != nil { + c.d.Logger(). + WithError(err). + WithField("message_id", msg.ID). + WithField("message_nid", msg.NID). + Error(`Unable to reset the retried message's status to "abandoned".`) + return err + } + } + + return errors.WithStack(herodot.ErrInternalServerError. + WithError(err.Error()).WithReason("failed to send email via smtp")) + } + + c.d.Logger(). + WithField("message_id", msg.ID). + WithField("message_nid", msg.NID). + WithField("message_type", msg.Type). + WithField("message_template_type", msg.TemplateType). + WithField("message_subject", msg.Subject). + Debug("Courier sent out message.") + + return nil +} diff --git a/courier/smtp_test.go b/courier/smtp_test.go index c30d865b88ec..107ab2803447 100644 --- a/courier/smtp_test.go +++ b/courier/smtp_test.go @@ -8,6 +8,7 @@ import ( "crypto/rand" "crypto/rsa" "crypto/tls" + "crypto/x509" "crypto/x509/pkix" "encoding/pem" "flag" @@ -19,8 +20,6 @@ import ( "testing" "time" - "crypto/x509" - "github.com/gofrs/uuid" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -40,13 +39,13 @@ func TestNewSMTP(t *testing.T) { ctx := context.Background() conf, reg := internal.NewFastRegistryWithMocks(t) - setupCourier := func(stringURL string) courier.Courier { + setupSMTPClient := func(stringURL string) *courier.SMTPClient { conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, stringURL) - u, err := conf.CourierSMTPURL(ctx) - require.NoError(t, err) - t.Logf("SMTP URL: %s", u.String()) - c, err := courier.NewCourier(ctx, reg) + channels, err := conf.CourierChannels(ctx) + require.NoError(t, err) + require.Len(t, channels, 1) + c, err := courier.NewSMTPClient(reg, channels[0].SMTPConfig) require.NoError(t, err) return c } @@ -55,21 +54,21 @@ func TestNewSMTP(t *testing.T) { t.SkipNow() } - //Should enforce StartTLS => dialer.StartTLSPolicy = gomail.MandatoryStartTLS and dialer.SSL = false - smtp := setupCourier("smtp://foo:bar@my-server:1234/") - assert.Equal(t, smtp.SmtpDialer().StartTLSPolicy, gomail.MandatoryStartTLS, "StartTLS not enforced") - assert.Equal(t, smtp.SmtpDialer().SSL, false, "Implicit TLS should not be enabled") + // Should enforce StartTLS => dialer.StartTLSPolicy = gomail.MandatoryStartTLS and dialer.SSL = false + smtp := setupSMTPClient("smtp://foo:bar@my-server:1234/") + assert.Equal(t, smtp.StartTLSPolicy, gomail.MandatoryStartTLS, "StartTLS not enforced") + assert.Equal(t, smtp.SSL, false, "Implicit TLS should not be enabled") - //Should enforce TLS => dialer.SSL = true - smtp = setupCourier("smtps://foo:bar@my-server:1234/") - assert.Equal(t, smtp.SmtpDialer().SSL, true, "Implicit TLS should be enabled") + // Should enforce TLS => dialer.SSL = true + smtp = setupSMTPClient("smtps://foo:bar@my-server:1234/") + assert.Equal(t, smtp.SSL, true, "Implicit TLS should be enabled") - //Should allow cleartext => dialer.StartTLSPolicy = gomail.OpportunisticStartTLS and dialer.SSL = false - smtp = setupCourier("smtp://foo:bar@my-server:1234/?disable_starttls=true") - assert.Equal(t, smtp.SmtpDialer().StartTLSPolicy, gomail.OpportunisticStartTLS, "StartTLS is enforced") - assert.Equal(t, smtp.SmtpDialer().SSL, false, "Implicit TLS should not be enabled") + // Should allow cleartext => dialer.StartTLSPolicy = gomail.OpportunisticStartTLS and dialer.SSL = false + smtp = setupSMTPClient("smtp://foo:bar@my-server:1234/?disable_starttls=true") + assert.Equal(t, smtp.StartTLSPolicy, gomail.OpportunisticStartTLS, "StartTLS is enforced") + assert.Equal(t, smtp.SSL, false, "Implicit TLS should not be enabled") - //Test cert based SMTP client auth + // Test cert based SMTP client auth clientCert, clientKey, err := generateTestClientCert() require.NoError(t, err) defer os.Remove(clientCert.Name()) @@ -81,17 +80,17 @@ func TestNewSMTP(t *testing.T) { clientPEM, err := tls.LoadX509KeyPair(clientCert.Name(), clientKey.Name()) require.NoError(t, err) - smtpWithCert := setupCourier("smtps://subdomain.my-server:1234/?server_name=my-server") - assert.Equal(t, smtpWithCert.SmtpDialer().SSL, true, "Implicit TLS should be enabled") - assert.Equal(t, smtpWithCert.SmtpDialer().Host, "subdomain.my-server", "SMTP Dialer host should match") - assert.Equal(t, smtpWithCert.SmtpDialer().TLSConfig.ServerName, "my-server", "TLS config server name should match") - assert.Equal(t, smtpWithCert.SmtpDialer().TLSConfig.ServerName, "my-server", "TLS config server name should match") - assert.Contains(t, smtpWithCert.SmtpDialer().TLSConfig.Certificates, clientPEM, "TLS config should contain client pem") - - //error case: invalid client key - conf.Set(ctx, config.ViperKeyCourierSMTPClientKeyPath, clientCert.Name()) //mixup client key and client cert - smtpWithCert = setupCourier("smtps://subdomain.my-server:1234/?server_name=my-server") - assert.Equal(t, len(smtpWithCert.SmtpDialer().TLSConfig.Certificates), 0, "TLS config certificates should be empty") + smtpWithCert := setupSMTPClient("smtps://subdomain.my-server:1234/?server_name=my-server") + assert.Equal(t, smtpWithCert.SSL, true, "Implicit TLS should be enabled") + assert.Equal(t, smtpWithCert.Host, "subdomain.my-server", "SMTP Dialer host should match") + assert.Equal(t, smtpWithCert.TLSConfig.ServerName, "my-server", "TLS config server name should match") + assert.Equal(t, smtpWithCert.TLSConfig.ServerName, "my-server", "TLS config server name should match") + assert.Contains(t, smtpWithCert.TLSConfig.Certificates, clientPEM, "TLS config should contain client pem") + + // error case: invalid client key + conf.Set(ctx, config.ViperKeyCourierSMTPClientKeyPath, clientCert.Name()) // mixup client key and client cert + smtpWithCert = setupSMTPClient("smtps://subdomain.my-server:1234/?server_name=my-server") + assert.Equal(t, len(smtpWithCert.TLSConfig.Certificates), 0, "TLS config certificates should be empty") } func TestQueueEmail(t *testing.T) { @@ -117,6 +116,13 @@ func TestQueueEmail(t *testing.T) { ctx, cancel := context.WithCancel(ctx) defer cancel() + _, err = c.QueueEmail(ctx, templates.NewTestStub(reg, &templates.TestStubModel{ + To: "invalid-email", + Subject: "test-subject-1", + Body: "test-body-1", + })) + require.Error(t, err) + id, err := c.QueueEmail(ctx, templates.NewTestStub(reg, &templates.TestStubModel{ To: "test-recipient-1@example.org", Subject: "test-subject-1", diff --git a/courier/stub/request.config.twilio.jsonnet b/courier/stub/request.config.twilio.jsonnet index da0736b06df0..f3a99db694e5 100644 --- a/courier/stub/request.config.twilio.jsonnet +++ b/courier/stub/request.config.twilio.jsonnet @@ -1,5 +1,5 @@ function(ctx) { - from: ctx.from, - to: ctx.to, - body: ctx.body + from: "Kratos Test", + to: ctx.Recipient, + body: ctx.Body } diff --git a/courier/template/courier/builtin/templates/verification_code/valid/sms.body.gotmpl b/courier/template/courier/builtin/templates/verification_code/valid/sms.body.gotmpl new file mode 100644 index 000000000000..0469598d2d58 --- /dev/null +++ b/courier/template/courier/builtin/templates/verification_code/valid/sms.body.gotmpl @@ -0,0 +1 @@ +Your verification code is: {{ .VerificationCode }} diff --git a/courier/template/email/login_code_valid.go b/courier/template/email/login_code_valid.go index 2debc3a0cb7c..1b3b55d4807c 100644 --- a/courier/template/email/login_code_valid.go +++ b/courier/template/email/login_code_valid.go @@ -49,3 +49,7 @@ func (t *LoginCodeValid) EmailBodyPlaintext(ctx context.Context) (string, error) func (t *LoginCodeValid) MarshalJSON() ([]byte, error) { return json.Marshal(t.model) } + +func (t *LoginCodeValid) TemplateType() template.TemplateType { + return template.TypeLoginCodeValid +} diff --git a/courier/template/email/login_code_valid_test.go b/courier/template/email/login_code_valid_test.go index dca97defe08c..eb6a95be0686 100644 --- a/courier/template/email/login_code_valid_test.go +++ b/courier/template/email/login_code_valid_test.go @@ -7,7 +7,7 @@ import ( "context" "testing" - "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template" "github.com/ory/kratos/courier/template/email" "github.com/ory/kratos/courier/template/testhelpers" "github.com/ory/kratos/internal" @@ -25,6 +25,6 @@ func TestLoginCodeValid(t *testing.T) { }) t.Run("test=with remote resources", func(t *testing.T) { - testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/login_code/valid", courier.TypeLoginCodeValid) + testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/login_code/valid", template.TypeLoginCodeValid) }) } diff --git a/courier/template/email/recovery_code_invalid.go b/courier/template/email/recovery_code_invalid.go index 01dad94cb0a9..4914b75c7024 100644 --- a/courier/template/email/recovery_code_invalid.go +++ b/courier/template/email/recovery_code_invalid.go @@ -50,3 +50,7 @@ func (t *RecoveryCodeInvalid) EmailBodyPlaintext(ctx context.Context) (string, e func (t *RecoveryCodeInvalid) MarshalJSON() ([]byte, error) { return json.Marshal(t.model) } + +func (t *RecoveryCodeInvalid) TemplateType() template.TemplateType { + return template.TypeRecoveryCodeInvalid +} diff --git a/courier/template/email/recovery_code_invalid_test.go b/courier/template/email/recovery_code_invalid_test.go index dc6e3d28cae6..b043ecbbfe38 100644 --- a/courier/template/email/recovery_code_invalid_test.go +++ b/courier/template/email/recovery_code_invalid_test.go @@ -7,7 +7,7 @@ import ( "context" "testing" - "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template" "github.com/ory/kratos/courier/template/email" "github.com/ory/kratos/courier/template/testhelpers" "github.com/ory/kratos/internal" @@ -25,6 +25,6 @@ func TestRecoveryCodeInvalid(t *testing.T) { }) t.Run("case=test remote resources", func(t *testing.T) { - testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/recovery_code/invalid", courier.TypeRecoveryCodeInvalid) + testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/recovery_code/invalid", template.TypeRecoveryCodeInvalid) }) } diff --git a/courier/template/email/recovery_code_valid.go b/courier/template/email/recovery_code_valid.go index 5468ac70206b..dce31b72e2fa 100644 --- a/courier/template/email/recovery_code_valid.go +++ b/courier/template/email/recovery_code_valid.go @@ -49,3 +49,7 @@ func (t *RecoveryCodeValid) EmailBodyPlaintext(ctx context.Context) (string, err func (t *RecoveryCodeValid) MarshalJSON() ([]byte, error) { return json.Marshal(t.model) } + +func (t *RecoveryCodeValid) TemplateType() template.TemplateType { + return template.TypeRecoveryCodeValid +} diff --git a/courier/template/email/recovery_code_valid_test.go b/courier/template/email/recovery_code_valid_test.go index 346e4180a93a..dd133c38c850 100644 --- a/courier/template/email/recovery_code_valid_test.go +++ b/courier/template/email/recovery_code_valid_test.go @@ -7,7 +7,7 @@ import ( "context" "testing" - "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template" "github.com/ory/kratos/courier/template/email" "github.com/ory/kratos/courier/template/testhelpers" "github.com/ory/kratos/internal" @@ -25,6 +25,6 @@ func TestRecoveryCodeValid(t *testing.T) { }) t.Run("test=with remote resources", func(t *testing.T) { - testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/recovery_code/valid", courier.TypeRecoveryCodeValid) + testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/recovery_code/valid", template.TypeRecoveryCodeValid) }) } diff --git a/courier/template/email/recovery_invalid.go b/courier/template/email/recovery_invalid.go index fec6edb7e20c..dbc8992d5593 100644 --- a/courier/template/email/recovery_invalid.go +++ b/courier/template/email/recovery_invalid.go @@ -47,3 +47,7 @@ func (t *RecoveryInvalid) EmailBodyPlaintext(ctx context.Context) (string, error func (t *RecoveryInvalid) MarshalJSON() ([]byte, error) { return json.Marshal(t.m) } + +func (t *RecoveryInvalid) TemplateType() template.TemplateType { + return template.TypeRecoveryInvalid +} diff --git a/courier/template/email/recovery_invalid_test.go b/courier/template/email/recovery_invalid_test.go index cae880a24e97..b39a3839ef89 100644 --- a/courier/template/email/recovery_invalid_test.go +++ b/courier/template/email/recovery_invalid_test.go @@ -7,7 +7,7 @@ import ( "context" "testing" - "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template" "github.com/ory/kratos/courier/template/email" "github.com/ory/kratos/courier/template/testhelpers" "github.com/ory/kratos/internal" @@ -25,6 +25,6 @@ func TestRecoverInvalid(t *testing.T) { }) t.Run("case=test remote resources", func(t *testing.T) { - testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/recovery/invalid", courier.TypeRecoveryInvalid) + testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/recovery/invalid", template.TypeRecoveryInvalid) }) } diff --git a/courier/template/email/recovery_valid.go b/courier/template/email/recovery_valid.go index 419cd26dd0c6..532d8d1bc4e6 100644 --- a/courier/template/email/recovery_valid.go +++ b/courier/template/email/recovery_valid.go @@ -49,3 +49,7 @@ func (t *RecoveryValid) EmailBodyPlaintext(ctx context.Context) (string, error) func (t *RecoveryValid) MarshalJSON() ([]byte, error) { return json.Marshal(t.m) } + +func (t *RecoveryValid) TemplateType() template.TemplateType { + return template.TypeRecoveryValid +} diff --git a/courier/template/email/recovery_valid_test.go b/courier/template/email/recovery_valid_test.go index eaaf85c27039..83027c787182 100644 --- a/courier/template/email/recovery_valid_test.go +++ b/courier/template/email/recovery_valid_test.go @@ -7,7 +7,7 @@ import ( "context" "testing" - "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template" "github.com/ory/kratos/courier/template/email" "github.com/ory/kratos/courier/template/testhelpers" "github.com/ory/kratos/internal" @@ -25,6 +25,6 @@ func TestRecoverValid(t *testing.T) { }) t.Run("test=with remote resources", func(t *testing.T) { - testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/recovery/valid", courier.TypeRecoveryValid) + testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/recovery/valid", template.TypeRecoveryValid) }) } diff --git a/courier/template/email/registration_code_valid.go b/courier/template/email/registration_code_valid.go index f7e39e334976..f984ffaeb6c6 100644 --- a/courier/template/email/registration_code_valid.go +++ b/courier/template/email/registration_code_valid.go @@ -49,3 +49,7 @@ func (t *RegistrationCodeValid) EmailBodyPlaintext(ctx context.Context) (string, func (t *RegistrationCodeValid) MarshalJSON() ([]byte, error) { return json.Marshal(t.model) } + +func (t *RegistrationCodeValid) TemplateType() template.TemplateType { + return template.TypeRegistrationCodeValid +} diff --git a/courier/template/email/registration_code_valid_test.go b/courier/template/email/registration_code_valid_test.go index be4cfe8059ea..c0a877698aa8 100644 --- a/courier/template/email/registration_code_valid_test.go +++ b/courier/template/email/registration_code_valid_test.go @@ -7,7 +7,7 @@ import ( "context" "testing" - "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template" "github.com/ory/kratos/courier/template/email" "github.com/ory/kratos/courier/template/testhelpers" "github.com/ory/kratos/internal" @@ -25,6 +25,6 @@ func TestRegistrationCodeValid(t *testing.T) { }) t.Run("test=with remote resources", func(t *testing.T) { - testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/registration_code/valid", courier.TypeRegistrationCodeValid) + testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/registration_code/valid", template.TypeRegistrationCodeValid) }) } diff --git a/courier/template/email/stub.go b/courier/template/email/stub.go index b96e1c195e22..57b66e06e4aa 100644 --- a/courier/template/email/stub.go +++ b/courier/template/email/stub.go @@ -49,3 +49,7 @@ func (t *TestStub) EmailBodyPlaintext(ctx context.Context) (string, error) { func (t *TestStub) MarshalJSON() ([]byte, error) { return json.Marshal(t.m) } + +func (t *TestStub) TemplateType() template.TemplateType { + return template.TypeTestStub +} diff --git a/courier/template/email/verification_code_invalid.go b/courier/template/email/verification_code_invalid.go index ea69695ce801..fa76441b3434 100644 --- a/courier/template/email/verification_code_invalid.go +++ b/courier/template/email/verification_code_invalid.go @@ -71,3 +71,7 @@ func (t *VerificationCodeInvalid) EmailBodyPlaintext(ctx context.Context) (strin func (t *VerificationCodeInvalid) MarshalJSON() ([]byte, error) { return json.Marshal(t.m) } + +func (t *VerificationCodeInvalid) TemplateType() template.TemplateType { + return template.TypeVerificationCodeInvalid +} diff --git a/courier/template/email/verification_code_invalid_test.go b/courier/template/email/verification_code_invalid_test.go index af36f87f5ce5..3764e64cf1bf 100644 --- a/courier/template/email/verification_code_invalid_test.go +++ b/courier/template/email/verification_code_invalid_test.go @@ -7,7 +7,7 @@ import ( "context" "testing" - "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template" "github.com/ory/kratos/courier/template/email" "github.com/ory/kratos/courier/template/testhelpers" "github.com/ory/kratos/internal" @@ -25,6 +25,6 @@ func TestVerifyCodeInvalid(t *testing.T) { }) t.Run("test=with remote resources", func(t *testing.T) { - testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/verification_code/invalid", courier.TypeVerificationCodeInvalid) + testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/verification_code/invalid", template.TypeVerificationCodeInvalid) }) } diff --git a/courier/template/email/verification_code_valid.go b/courier/template/email/verification_code_valid.go index fdb752c791ba..e23f48f70b5a 100644 --- a/courier/template/email/verification_code_valid.go +++ b/courier/template/email/verification_code_valid.go @@ -72,3 +72,7 @@ func (t *VerificationCodeValid) EmailBodyPlaintext(ctx context.Context) (string, func (t *VerificationCodeValid) MarshalJSON() ([]byte, error) { return json.Marshal(t.m) } + +func (t *VerificationCodeValid) TemplateType() template.TemplateType { + return template.TypeVerificationCodeValid +} diff --git a/courier/template/email/verification_code_valid_test.go b/courier/template/email/verification_code_valid_test.go index 7b2883927074..6ec0857ea2bb 100644 --- a/courier/template/email/verification_code_valid_test.go +++ b/courier/template/email/verification_code_valid_test.go @@ -7,7 +7,7 @@ import ( "context" "testing" - "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template" "github.com/ory/kratos/courier/template/email" "github.com/ory/kratos/courier/template/testhelpers" "github.com/ory/kratos/internal" @@ -25,6 +25,6 @@ func TestVerifyCodeValid(t *testing.T) { }) t.Run("test=with remote resources", func(t *testing.T) { - testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/verification_code/valid", courier.TypeVerificationCodeValid) + testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/verification_code/valid", template.TypeVerificationCodeValid) }) } diff --git a/courier/template/email/verification_invalid.go b/courier/template/email/verification_invalid.go index c62ec0523c3f..5f6623f04352 100644 --- a/courier/template/email/verification_invalid.go +++ b/courier/template/email/verification_invalid.go @@ -47,3 +47,7 @@ func (t *VerificationInvalid) EmailBodyPlaintext(ctx context.Context) (string, e func (t *VerificationInvalid) MarshalJSON() ([]byte, error) { return json.Marshal(t.m) } + +func (t *VerificationInvalid) TemplateType() template.TemplateType { + return template.TypeVerificationInvalid +} diff --git a/courier/template/email/verification_invalid_test.go b/courier/template/email/verification_invalid_test.go index 226dc39aede0..1950b3d0806c 100644 --- a/courier/template/email/verification_invalid_test.go +++ b/courier/template/email/verification_invalid_test.go @@ -7,7 +7,7 @@ import ( "context" "testing" - "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template" "github.com/ory/kratos/courier/template/email" "github.com/ory/kratos/courier/template/testhelpers" "github.com/ory/kratos/internal" @@ -26,7 +26,7 @@ func TestVerifyInvalid(t *testing.T) { t.Run("test=with remote resources", func(t *testing.T) { t.Run("test=with remote resources", func(t *testing.T) { - testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/verification/invalid", courier.TypeVerificationInvalid) + testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/verification/invalid", template.TypeVerificationInvalid) }) }) } diff --git a/courier/template/email/verification_valid.go b/courier/template/email/verification_valid.go index d58710da86cb..723ea618e071 100644 --- a/courier/template/email/verification_valid.go +++ b/courier/template/email/verification_valid.go @@ -49,3 +49,7 @@ func (t *VerificationValid) EmailBodyPlaintext(ctx context.Context) (string, err func (t *VerificationValid) MarshalJSON() ([]byte, error) { return json.Marshal(t.m) } + +func (t *VerificationValid) TemplateType() template.TemplateType { + return template.TypeVerificationValid +} diff --git a/courier/template/email/verification_valid_test.go b/courier/template/email/verification_valid_test.go index 6c086fbdb6a7..334ac07d1ab8 100644 --- a/courier/template/email/verification_valid_test.go +++ b/courier/template/email/verification_valid_test.go @@ -7,7 +7,7 @@ import ( "context" "testing" - "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template" "github.com/ory/kratos/courier/template/email" "github.com/ory/kratos/courier/template/testhelpers" "github.com/ory/kratos/internal" @@ -25,6 +25,6 @@ func TestVerifyValid(t *testing.T) { }) t.Run("test=with remote resources", func(t *testing.T) { - testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/verification/valid", courier.TypeVerificationValid) + testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/verification/valid", template.TypeVerificationValid) }) } diff --git a/courier/template/sms/otp.go b/courier/template/sms/otp.go deleted file mode 100644 index 7dda3f4fb2c8..000000000000 --- a/courier/template/sms/otp.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright © 2023 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - -package sms - -import ( - "context" - "encoding/json" - "os" - - "github.com/ory/kratos/courier/template" -) - -type ( - OTPMessage struct { - d template.Dependencies - m *OTPMessageModel - } - - OTPMessageModel struct { - To string - Code string - Identity map[string]interface{} - } -) - -func NewOTPMessage(d template.Dependencies, m *OTPMessageModel) *OTPMessage { - return &OTPMessage{d: d, m: m} -} - -func (t *OTPMessage) PhoneNumber() (string, error) { - return t.m.To, nil -} - -func (t *OTPMessage) SMSBody(ctx context.Context) (string, error) { - return template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "otp/sms.body.gotmpl", "otp/sms.body*", t.m, "") -} - -func (t *OTPMessage) MarshalJSON() ([]byte, error) { - return json.Marshal(t.m) -} diff --git a/courier/template/sms/stub.go b/courier/template/sms/stub.go index b42d76c11319..0d68db1b209b 100644 --- a/courier/template/sms/stub.go +++ b/courier/template/sms/stub.go @@ -39,3 +39,7 @@ func (t *TestStub) SMSBody(ctx context.Context) (string, error) { func (t *TestStub) MarshalJSON() ([]byte, error) { return json.Marshal(t.m) } + +func (t *TestStub) TemplateType() template.TemplateType { + return template.TypeTestStub +} diff --git a/courier/template/sms/verification_code.go b/courier/template/sms/verification_code.go new file mode 100644 index 000000000000..f4ab6fc23359 --- /dev/null +++ b/courier/template/sms/verification_code.go @@ -0,0 +1,53 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sms + +import ( + "context" + "encoding/json" + "os" + + "github.com/ory/kratos/courier/template" +) + +type ( + VerificationCodeValid struct { + deps template.Dependencies + model *VerificationCodeValidModel + } + + VerificationCodeValidModel struct { + To string + VerificationCode string + Identity map[string]interface{} + } +) + +func NewVerificationCodeValid(d template.Dependencies, m *VerificationCodeValidModel) *VerificationCodeValid { + return &VerificationCodeValid{deps: d, model: m} +} + +func (t *VerificationCodeValid) PhoneNumber() (string, error) { + return t.model.To, nil +} + +func (t *VerificationCodeValid) SMSBody(ctx context.Context) (string, error) { + return template.LoadText( + ctx, + t.deps, + os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), + "verification_code/valid/sms.body.gotmpl", + "verification_code/valid/sms.body*", + t.model, + t.deps.CourierConfig().CourierSMSTemplatesVerificationCodeValid(ctx).Body.PlainText, + ) +} + +func (t *VerificationCodeValid) MarshalJSON() ([]byte, error) { + return json.Marshal(t.model) +} + +func (t *VerificationCodeValid) TemplateType() template.TemplateType { + return template.TypeVerificationCodeValid +} diff --git a/courier/template/sms/otp_test.go b/courier/template/sms/verification_code_test.go similarity index 86% rename from courier/template/sms/otp_test.go rename to courier/template/sms/verification_code_test.go index 9853c10fac34..fc2bb892e6b2 100644 --- a/courier/template/sms/otp_test.go +++ b/courier/template/sms/verification_code_test.go @@ -23,7 +23,7 @@ func TestNewOTPMessage(t *testing.T) { otp = "012345" ) - tpl := sms.NewOTPMessage(reg, &sms.OTPMessageModel{To: expectedPhone, Code: otp}) + tpl := sms.NewVerificationCodeValid(reg, &sms.VerificationCodeValidModel{To: expectedPhone, VerificationCode: otp}) expectedBody := fmt.Sprintf("Your verification code is: %s\n", otp) diff --git a/courier/template/template.go b/courier/template/template.go index 483c40bd2f5e..bf7074990253 100644 --- a/courier/template/template.go +++ b/courier/template/template.go @@ -12,19 +12,7 @@ import ( "github.com/ory/x/httpx" ) -type ( - Config interface { - CourierTemplatesRoot() string - CourierTemplatesVerificationInvalid() *config.CourierEmailTemplate - CourierTemplatesVerificationValid() *config.CourierEmailTemplate - CourierTemplatesRecoveryInvalid() *config.CourierEmailTemplate - CourierTemplatesRecoveryValid() *config.CourierEmailTemplate - CourierTemplatesLoginValid() *config.CourierEmailTemplate - CourierTemplatesRegistrationValid() *config.CourierEmailTemplate - } - - Dependencies interface { - CourierConfig() config.CourierConfigs - HTTPClient(ctx context.Context, opts ...httpx.ResilientOptions) *retryablehttp.Client - } -) +type Dependencies interface { + CourierConfig() config.CourierConfigs + HTTPClient(ctx context.Context, opts ...httpx.ResilientOptions) *retryablehttp.Client +} diff --git a/courier/template/testhelpers/testhelpers.go b/courier/template/testhelpers/testhelpers.go index 6c2923dbe416..66d2f15f6f4b 100644 --- a/courier/template/testhelpers/testhelpers.go +++ b/courier/template/testhelpers/testhelpers.go @@ -18,7 +18,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/ory/kratos/courier" "github.com/ory/kratos/courier/template" "github.com/ory/kratos/driver" "github.com/ory/kratos/driver/config" @@ -51,7 +50,7 @@ func TestRendered(t *testing.T, ctx context.Context, tpl interface { assert.NotEmpty(t, rendered) } -func TestRemoteTemplates(t *testing.T, basePath string, tmplType courier.TemplateType) { +func TestRemoteTemplates(t *testing.T, basePath string, tmplType template.TemplateType) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) @@ -61,32 +60,32 @@ func TestRemoteTemplates(t *testing.T, basePath string, tmplType courier.Templat return base64.StdEncoding.EncodeToString(f) } - getTemplate := func(tmpl courier.TemplateType, d template.Dependencies) interface { + getTemplate := func(tmpl template.TemplateType, d template.Dependencies) interface { EmailBody(context.Context) (string, error) EmailSubject(context.Context) (string, error) } { switch tmpl { - case courier.TypeRecoveryInvalid: + case template.TypeRecoveryInvalid: return email.NewRecoveryInvalid(d, &email.RecoveryInvalidModel{}) - case courier.TypeRecoveryValid: + case template.TypeRecoveryValid: return email.NewRecoveryValid(d, &email.RecoveryValidModel{}) - case courier.TypeRecoveryCodeValid: + case template.TypeRecoveryCodeValid: return email.NewRecoveryCodeValid(d, &email.RecoveryCodeValidModel{}) - case courier.TypeRecoveryCodeInvalid: + case template.TypeRecoveryCodeInvalid: return email.NewRecoveryCodeInvalid(d, &email.RecoveryCodeInvalidModel{}) - case courier.TypeTestStub: + case template.TypeTestStub: return email.NewTestStub(d, &email.TestStubModel{}) - case courier.TypeVerificationInvalid: + case template.TypeVerificationInvalid: return email.NewVerificationInvalid(d, &email.VerificationInvalidModel{}) - case courier.TypeVerificationValid: + case template.TypeVerificationValid: return email.NewVerificationValid(d, &email.VerificationValidModel{}) - case courier.TypeVerificationCodeInvalid: + case template.TypeVerificationCodeInvalid: return email.NewVerificationCodeInvalid(d, &email.VerificationCodeInvalidModel{}) - case courier.TypeVerificationCodeValid: + case template.TypeVerificationCodeValid: return email.NewVerificationCodeValid(d, &email.VerificationCodeValidModel{}) - case courier.TypeLoginCodeValid: + case template.TypeLoginCodeValid: return email.NewLoginCodeValid(d, &email.LoginCodeValidModel{}) - case courier.TypeRegistrationCodeValid: + case template.TypeRegistrationCodeValid: return email.NewRegistrationCodeValid(d, &email.RegistrationCodeValidModel{}) default: return nil diff --git a/courier/template/type.go b/courier/template/type.go new file mode 100644 index 000000000000..4fc0b9bccca2 --- /dev/null +++ b/courier/template/type.go @@ -0,0 +1,23 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package template + +// A Template's type +// +// swagger:enum TemplateType +type TemplateType string + +const ( + TypeRecoveryInvalid TemplateType = "recovery_invalid" + TypeRecoveryValid TemplateType = "recovery_valid" + TypeRecoveryCodeInvalid TemplateType = "recovery_code_invalid" + TypeRecoveryCodeValid TemplateType = "recovery_code_valid" + TypeVerificationInvalid TemplateType = "verification_invalid" + TypeVerificationValid TemplateType = "verification_valid" + TypeVerificationCodeInvalid TemplateType = "verification_code_invalid" + TypeVerificationCodeValid TemplateType = "verification_code_valid" + TypeTestStub TemplateType = "stub" + TypeLoginCodeValid TemplateType = "login_code_valid" + TypeRegistrationCodeValid TemplateType = "registration_code_valid" +) diff --git a/courier/email_templates.go b/courier/templates.go similarity index 55% rename from courier/email_templates.go rename to courier/templates.go index d2bae0a197e4..4ffb2c3f766a 100644 --- a/courier/email_templates.go +++ b/courier/templates.go @@ -15,8 +15,13 @@ import ( ) type ( - EmailTemplate interface { + Template interface { json.Marshaler + TemplateType() template.TemplateType + } + + EmailTemplate interface { + Template EmailSubject(context.Context) (string, error) EmailBody(context.Context) (string, error) EmailBodyPlaintext(context.Context) (string, error) @@ -24,118 +29,69 @@ type ( } ) -// A Template's type -// -// swagger:enum TemplateType -type TemplateType string - -const ( - TypeRecoveryInvalid TemplateType = "recovery_invalid" - TypeRecoveryValid TemplateType = "recovery_valid" - TypeRecoveryCodeInvalid TemplateType = "recovery_code_invalid" - TypeRecoveryCodeValid TemplateType = "recovery_code_valid" - TypeVerificationInvalid TemplateType = "verification_invalid" - TypeVerificationValid TemplateType = "verification_valid" - TypeVerificationCodeInvalid TemplateType = "verification_code_invalid" - TypeVerificationCodeValid TemplateType = "verification_code_valid" - TypeOTP TemplateType = "otp" - TypeTestStub TemplateType = "stub" - TypeLoginCodeValid TemplateType = "login_code_valid" - TypeRegistrationCodeValid TemplateType = "registration_code_valid" -) - -func GetEmailTemplateType(t EmailTemplate) (TemplateType, error) { - switch t.(type) { - case *email.RecoveryInvalid: - return TypeRecoveryInvalid, nil - case *email.RecoveryValid: - return TypeRecoveryValid, nil - case *email.RecoveryCodeInvalid: - return TypeRecoveryCodeInvalid, nil - case *email.RecoveryCodeValid: - return TypeRecoveryCodeValid, nil - case *email.VerificationInvalid: - return TypeVerificationInvalid, nil - case *email.VerificationValid: - return TypeVerificationValid, nil - case *email.VerificationCodeInvalid: - return TypeVerificationCodeInvalid, nil - case *email.VerificationCodeValid: - return TypeVerificationCodeValid, nil - case *email.LoginCodeValid: - return TypeLoginCodeValid, nil - case *email.RegistrationCodeValid: - return TypeRegistrationCodeValid, nil - case *email.TestStub: - return TypeTestStub, nil - default: - return "", errors.Errorf("unexpected template type") - } -} - func NewEmailTemplateFromMessage(d template.Dependencies, msg Message) (EmailTemplate, error) { switch msg.TemplateType { - case TypeRecoveryInvalid: + case template.TypeRecoveryInvalid: var t email.RecoveryInvalidModel if err := json.Unmarshal(msg.TemplateData, &t); err != nil { return nil, err } return email.NewRecoveryInvalid(d, &t), nil - case TypeRecoveryValid: + case template.TypeRecoveryValid: var t email.RecoveryValidModel if err := json.Unmarshal(msg.TemplateData, &t); err != nil { return nil, err } return email.NewRecoveryValid(d, &t), nil - case TypeRecoveryCodeInvalid: + case template.TypeRecoveryCodeInvalid: var t email.RecoveryCodeInvalidModel if err := json.Unmarshal(msg.TemplateData, &t); err != nil { return nil, err } return email.NewRecoveryCodeInvalid(d, &t), nil - case TypeRecoveryCodeValid: + case template.TypeRecoveryCodeValid: var t email.RecoveryCodeValidModel if err := json.Unmarshal(msg.TemplateData, &t); err != nil { return nil, err } return email.NewRecoveryCodeValid(d, &t), nil - case TypeVerificationInvalid: + case template.TypeVerificationInvalid: var t email.VerificationInvalidModel if err := json.Unmarshal(msg.TemplateData, &t); err != nil { return nil, err } return email.NewVerificationInvalid(d, &t), nil - case TypeVerificationValid: + case template.TypeVerificationValid: var t email.VerificationValidModel if err := json.Unmarshal(msg.TemplateData, &t); err != nil { return nil, err } return email.NewVerificationValid(d, &t), nil - case TypeVerificationCodeInvalid: + case template.TypeVerificationCodeInvalid: var t email.VerificationCodeInvalidModel if err := json.Unmarshal(msg.TemplateData, &t); err != nil { return nil, err } return email.NewVerificationCodeInvalid(d, &t), nil - case TypeVerificationCodeValid: + case template.TypeVerificationCodeValid: var t email.VerificationCodeValidModel if err := json.Unmarshal(msg.TemplateData, &t); err != nil { return nil, err } return email.NewVerificationCodeValid(d, &t), nil - case TypeTestStub: + case template.TypeTestStub: var t email.TestStubModel if err := json.Unmarshal(msg.TemplateData, &t); err != nil { return nil, err } return email.NewTestStub(d, &t), nil - case TypeLoginCodeValid: + case template.TypeLoginCodeValid: var t email.LoginCodeValidModel if err := json.Unmarshal(msg.TemplateData, &t); err != nil { return nil, err } return email.NewLoginCodeValid(d, &t), nil - case TypeRegistrationCodeValid: + case template.TypeRegistrationCodeValid: var t email.RegistrationCodeValidModel if err := json.Unmarshal(msg.TemplateData, &t); err != nil { return nil, err diff --git a/courier/templates_test.go b/courier/templates_test.go new file mode 100644 index 000000000000..f6b9fa3f8b7b --- /dev/null +++ b/courier/templates_test.go @@ -0,0 +1,72 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package courier_test + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/internal" +) + +func TestNewEmailTemplateFromMessage(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + ctx := context.Background() + + for tmplType, expectedTmpl := range map[template.TemplateType]courier.EmailTemplate{ + template.TypeRecoveryInvalid: email.NewRecoveryInvalid(reg, &email.RecoveryInvalidModel{To: "foo"}), + template.TypeRecoveryValid: email.NewRecoveryValid(reg, &email.RecoveryValidModel{To: "bar", RecoveryURL: "http://foo.bar"}), + template.TypeRecoveryCodeValid: email.NewRecoveryCodeValid(reg, &email.RecoveryCodeValidModel{To: "bar", RecoveryCode: "12345678"}), + template.TypeRecoveryCodeInvalid: email.NewRecoveryCodeInvalid(reg, &email.RecoveryCodeInvalidModel{To: "bar"}), + template.TypeVerificationInvalid: email.NewVerificationInvalid(reg, &email.VerificationInvalidModel{To: "baz"}), + template.TypeVerificationValid: email.NewVerificationValid(reg, &email.VerificationValidModel{To: "faz", VerificationURL: "http://bar.foo"}), + template.TypeVerificationCodeInvalid: email.NewVerificationCodeInvalid(reg, &email.VerificationCodeInvalidModel{To: "baz"}), + template.TypeVerificationCodeValid: email.NewVerificationCodeValid(reg, &email.VerificationCodeValidModel{To: "faz", VerificationURL: "http://bar.foo", VerificationCode: "123456678"}), + template.TypeTestStub: email.NewTestStub(reg, &email.TestStubModel{To: "far", Subject: "test subject", Body: "test body"}), + template.TypeLoginCodeValid: email.NewLoginCodeValid(reg, &email.LoginCodeValidModel{To: "far", LoginCode: "123456"}), + template.TypeRegistrationCodeValid: email.NewRegistrationCodeValid(reg, &email.RegistrationCodeValidModel{To: "far", RegistrationCode: "123456"}), + } { + t.Run(fmt.Sprintf("case=%s", tmplType), func(t *testing.T) { + tmplData, err := json.Marshal(expectedTmpl) + require.NoError(t, err) + + m := courier.Message{TemplateType: tmplType, TemplateData: tmplData} + actualTmpl, err := courier.NewEmailTemplateFromMessage(reg, m) + require.NoError(t, err) + + require.IsType(t, expectedTmpl, actualTmpl) + + expectedRecipient, err := expectedTmpl.EmailRecipient() + require.NoError(t, err) + actualRecipient, err := actualTmpl.EmailRecipient() + require.NoError(t, err) + require.Equal(t, expectedRecipient, actualRecipient) + + expectedSubject, err := expectedTmpl.EmailSubject(ctx) + require.NoError(t, err) + actualSubject, err := actualTmpl.EmailSubject(ctx) + require.NoError(t, err) + require.Equal(t, expectedSubject, actualSubject) + + expectedBody, err := expectedTmpl.EmailBody(ctx) + require.NoError(t, err) + actualBody, err := actualTmpl.EmailBody(ctx) + require.NoError(t, err) + require.Equal(t, expectedBody, actualBody) + + expectedBodyPlaintext, err := expectedTmpl.EmailBodyPlaintext(ctx) + require.NoError(t, err) + actualBodyPlaintext, err := actualTmpl.EmailBodyPlaintext(ctx) + require.NoError(t, err) + require.Equal(t, expectedBodyPlaintext, actualBodyPlaintext) + }) + } +} diff --git a/driver/config/config.go b/driver/config/config.go index a5ff6c7fb3b8..35d33ee8316a 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -18,6 +18,8 @@ import ( "testing" "time" + "go.opentelemetry.io/otel/trace/noop" + "github.com/ory/x/crdbx" "github.com/go-webauthn/webauthn/protocol" @@ -27,7 +29,6 @@ import ( "github.com/pkg/errors" "github.com/rs/cors" "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/trace" "golang.org/x/net/publicsuffix" "github.com/ory/herodot" @@ -65,20 +66,20 @@ const ( ViperKeyCourierTemplatesVerificationValidEmail = "courier.templates.verification.valid.email" ViperKeyCourierTemplatesVerificationCodeInvalidEmail = "courier.templates.verification_code.invalid.email" ViperKeyCourierTemplatesVerificationCodeValidEmail = "courier.templates.verification_code.valid.email" + ViperKeyCourierTemplatesVerificationCodeValidSMS = "courier.templates.verification_code.valid.sms" ViperKeyCourierDeliveryStrategy = "courier.delivery_strategy" ViperKeyCourierHTTPRequestConfig = "courier.http.request_config" ViperKeyCourierTemplatesLoginCodeValidEmail = "courier.templates.login_code.valid.email" ViperKeyCourierTemplatesRegistrationCodeValidEmail = "courier.templates.registration_code.valid.email" + ViperKeyCourierSMTP = "courier.smtp" ViperKeyCourierSMTPFrom = "courier.smtp.from_address" ViperKeyCourierSMTPFromName = "courier.smtp.from_name" ViperKeyCourierSMTPHeaders = "courier.smtp.headers" ViperKeyCourierSMTPLocalName = "courier.smtp.local_name" - ViperKeyCourierSMSRequestConfig = "courier.sms.request_config" - ViperKeyCourierSMSEnabled = "courier.sms.enabled" - ViperKeyCourierSMSFrom = "courier.sms.from" ViperKeyCourierMessageRetries = "courier.message_retries" ViperKeyCourierWorkerPullCount = "courier.worker.pull_count" ViperKeyCourierWorkerPullWait = "courier.worker.pull_wait" + ViperKeyCourierChannels = "courier.channels" ViperKeySecretsDefault = "secrets.default" ViperKeySecretsCookie = "secrets.cookie" ViperKeySecretsCipher = "secrets.cipher" @@ -257,6 +258,28 @@ type ( Body *CourierEmailBodyTemplate `json:"body"` Subject string `json:"subject"` } + CourierSMSTemplate struct { + Body *CourierSMSTemplateBody `json:"body"` + } + CourierSMSTemplateBody struct { + PlainText string `json:"plaintext"` + } + CourierChannel struct { + ID string `json:"id" koanf:"id"` + Type string `json:"type" koanf:"type"` + SMTPConfig *SMTPConfig `json:"smtp_config" koanf:"smtp_config"` + RequestConfig json.RawMessage `json:"request_config" koanf:"-"` + RequestConfigRaw map[string]any `json:"-" koanf:"request_config"` + } + SMTPConfig struct { + ConnectionURI string `json:"connection_uri" koanf:"connection_uri"` + ClientCertPath string `json:"client_cert_path" koanf:"client_cert_path"` + ClientKeyPath string `json:"client_key_path" koanf:"client_key_path"` + FromAddress string `json:"from_address" koanf:"from_address"` + FromName string `json:"from_name" koanf:"from_name"` + Headers map[string]string `json:"headers" koanf:"headers"` + LocalName string `json:"local_name" koanf:"local_name"` + } Config struct { l *logrusx.Logger p *configx.Provider @@ -268,18 +291,6 @@ type ( Config() *Config } CourierConfigs interface { - CourierEmailStrategy(ctx context.Context) string - CourierEmailRequestConfig(ctx context.Context) json.RawMessage - CourierSMTPURL(ctx context.Context) (*url.URL, error) - CourierSMTPClientCertPath(ctx context.Context) string - CourierSMTPClientKeyPath(ctx context.Context) string - CourierSMTPFrom(ctx context.Context) string - CourierSMTPFromName(ctx context.Context) string - CourierSMTPHeaders(ctx context.Context) map[string]string - CourierSMTPLocalName(ctx context.Context) string - CourierSMSEnabled(ctx context.Context) bool - CourierSMSFrom(ctx context.Context) string - CourierSMSRequestConfig(ctx context.Context) json.RawMessage CourierTemplatesRoot(ctx context.Context) string CourierTemplatesVerificationInvalid(ctx context.Context) *CourierEmailTemplate CourierTemplatesVerificationValid(ctx context.Context) *CourierEmailTemplate @@ -291,9 +302,11 @@ type ( CourierTemplatesVerificationCodeValid(ctx context.Context) *CourierEmailTemplate CourierTemplatesLoginCodeValid(ctx context.Context) *CourierEmailTemplate CourierTemplatesRegistrationCodeValid(ctx context.Context) *CourierEmailTemplate + CourierSMSTemplatesVerificationCodeValid(ctx context.Context) *CourierSMSTemplate CourierMessageRetries(ctx context.Context) int CourierWorkerPullCount(ctx context.Context) int CourierWorkerPullWait(ctx context.Context) time.Duration + CourierChannels(context.Context) ([]*CourierChannel, error) } ) @@ -426,7 +439,7 @@ func (p *Config) validateIdentitySchemas(ctx context.Context) error { // Tracing still works correctly even though we pass a no-op tracer // here, because the otelhttp package will preferentially use the // tracer from the incoming request context over this one. - httpx.ResilientClientWithTracer(trace.NewNoopTracerProvider().Tracer("github.com/ory/kratos/driver/config")), + httpx.ResilientClientWithTracer(noop.NewTracerProvider().Tracer("github.com/ory/kratos/driver/config")), } if o, ok := ctx.Value(validateIdentitySchemasClientKey).([]httpx.ResilientOptions); ok { @@ -883,15 +896,6 @@ func (p *Config) SelfAdminURL(ctx context.Context) *url.URL { return p.baseURL(ctx, ViperKeyAdminBaseURL, ViperKeyAdminHost, ViperKeyAdminPort, 4434) } -func (p *Config) CourierSMTPURL(ctx context.Context) (*url.URL, error) { - source := p.GetProvider(ctx).String(ViperKeyCourierSMTPURL) - parsed, err := url.Parse(source) - if err != nil { - return nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to parse the project's SMTP URL. Please ensure that it is properly escaped: https://www.ory.sh/dr/3").WithDebugf("%s", err)) - } - return parsed, nil -} - func (p *Config) OAuth2ProviderHeader(ctx context.Context) http.Header { hh := map[string]string{} if err := p.GetProvider(ctx).Unmarshal(ViperKeyOAuth2ProviderHeader, &hh); err != nil { @@ -1021,31 +1025,11 @@ func (p *Config) CourierEmailRequestConfig(ctx context.Context) json.RawMessage return config } -func (p *Config) CourierSMTPClientCertPath(ctx context.Context) string { - return p.GetProvider(ctx).StringF(ViperKeyCourierSMTPClientCertPath, "") -} - -func (p *Config) CourierSMTPClientKeyPath(ctx context.Context) string { - return p.GetProvider(ctx).StringF(ViperKeyCourierSMTPClientKeyPath, "") -} - -func (p *Config) CourierSMTPFrom(ctx context.Context) string { - return p.GetProvider(ctx).StringF(ViperKeyCourierSMTPFrom, "noreply@kratos.ory.sh") -} - -func (p *Config) CourierSMTPFromName(ctx context.Context) string { - return p.GetProvider(ctx).StringF(ViperKeyCourierSMTPFromName, "") -} - -func (p *Config) CourierSMTPLocalName(ctx context.Context) string { - return p.GetProvider(ctx).StringF(ViperKeyCourierSMTPLocalName, "localhost") -} - func (p *Config) CourierTemplatesRoot(ctx context.Context) string { return p.GetProvider(ctx).StringF(ViperKeyCourierTemplatesPath, "courier/builtin/templates") } -func (p *Config) CourierTemplatesHelper(ctx context.Context, key string) *CourierEmailTemplate { +func (p *Config) CourierEmailTemplatesHelper(ctx context.Context, key string) *CourierEmailTemplate { courierTemplate := &CourierEmailTemplate{ Body: &CourierEmailBodyTemplate{ PlainText: "", @@ -1071,44 +1055,72 @@ func (p *Config) CourierTemplatesHelper(ctx context.Context, key string) *Courie return courierTemplate } +func (p *Config) CourierSMSTemplatesHelper(ctx context.Context, key string) *CourierSMSTemplate { + courierTemplate := &CourierSMSTemplate{ + Body: &CourierSMSTemplateBody{ + PlainText: "", + }, + } + + if !p.GetProvider(ctx).Exists(key) { + return courierTemplate + } + + config, err := json.Marshal(p.GetProvider(ctx).Get(key)) + if err != nil { + p.l.WithError(err).Fatalf("Unable to decode values from %s.", key) + return courierTemplate + } + + if err := json.Unmarshal(config, courierTemplate); err != nil { + p.l.WithError(err).Fatalf("Unable to encode values from %s.", key) + return courierTemplate + } + return courierTemplate +} + func (p *Config) CourierTemplatesVerificationInvalid(ctx context.Context) *CourierEmailTemplate { - return p.CourierTemplatesHelper(ctx, ViperKeyCourierTemplatesVerificationInvalidEmail) + return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesVerificationInvalidEmail) } func (p *Config) CourierTemplatesVerificationValid(ctx context.Context) *CourierEmailTemplate { - return p.CourierTemplatesHelper(ctx, ViperKeyCourierTemplatesVerificationValidEmail) + return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesVerificationValidEmail) } func (p *Config) CourierTemplatesRecoveryInvalid(ctx context.Context) *CourierEmailTemplate { - return p.CourierTemplatesHelper(ctx, ViperKeyCourierTemplatesRecoveryInvalidEmail) + return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesRecoveryInvalidEmail) } func (p *Config) CourierTemplatesRecoveryValid(ctx context.Context) *CourierEmailTemplate { - return p.CourierTemplatesHelper(ctx, ViperKeyCourierTemplatesRecoveryValidEmail) + return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesRecoveryValidEmail) } func (p *Config) CourierTemplatesRecoveryCodeInvalid(ctx context.Context) *CourierEmailTemplate { - return p.CourierTemplatesHelper(ctx, ViperKeyCourierTemplatesRecoveryCodeInvalidEmail) + return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesRecoveryCodeInvalidEmail) } func (p *Config) CourierTemplatesRecoveryCodeValid(ctx context.Context) *CourierEmailTemplate { - return p.CourierTemplatesHelper(ctx, ViperKeyCourierTemplatesRecoveryCodeValidEmail) + return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesRecoveryCodeValidEmail) } func (p *Config) CourierTemplatesVerificationCodeInvalid(ctx context.Context) *CourierEmailTemplate { - return p.CourierTemplatesHelper(ctx, ViperKeyCourierTemplatesVerificationCodeInvalidEmail) + return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesVerificationCodeInvalidEmail) } func (p *Config) CourierTemplatesVerificationCodeValid(ctx context.Context) *CourierEmailTemplate { - return p.CourierTemplatesHelper(ctx, ViperKeyCourierTemplatesVerificationCodeValidEmail) + return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesVerificationCodeValidEmail) +} + +func (p *Config) CourierSMSTemplatesVerificationCodeValid(ctx context.Context) *CourierSMSTemplate { + return p.CourierSMSTemplatesHelper(ctx, ViperKeyCourierTemplatesVerificationCodeValidSMS) } func (p *Config) CourierTemplatesLoginCodeValid(ctx context.Context) *CourierEmailTemplate { - return p.CourierTemplatesHelper(ctx, ViperKeyCourierTemplatesLoginCodeValidEmail) + return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesLoginCodeValidEmail) } func (p *Config) CourierTemplatesRegistrationCodeValid(ctx context.Context) *CourierEmailTemplate { - return p.CourierTemplatesHelper(ctx, ViperKeyCourierTemplatesRegistrationCodeValidEmail) + return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesRegistrationCodeValidEmail) } func (p *Config) CourierMessageRetries(ctx context.Context) int { @@ -1127,25 +1139,40 @@ func (p *Config) CourierSMTPHeaders(ctx context.Context) map[string]string { return p.GetProvider(ctx).StringMap(ViperKeyCourierSMTPHeaders) } -func (p *Config) CourierSMSRequestConfig(ctx context.Context) json.RawMessage { - if !p.GetProvider(ctx).Bool(ViperKeyCourierSMSEnabled) { - return nil +func (p *Config) CourierChannels(ctx context.Context) (ccs []*CourierChannel, _ error) { + if err := p.GetProvider(ctx).Koanf.Unmarshal(ViperKeyCourierChannels, &ccs); err != nil { + return nil, errors.WithStack(err) } - - config, err := json.Marshal(p.GetProvider(ctx).Get(ViperKeyCourierSMSRequestConfig)) - if err != nil { - p.l.WithError(err).Warn("Unable to marshal SMS request configuration.") - return json.RawMessage("{}") + if len(ccs) != 0 { + for _, c := range ccs { + if c.RequestConfigRaw != nil { + var err error + c.RequestConfig, err = json.Marshal(c.RequestConfigRaw) + if err != nil { + return nil, errors.WithStack(err) + } + } + } + return ccs, nil } - return config -} -func (p *Config) CourierSMSFrom(ctx context.Context) string { - return p.GetProvider(ctx).StringF(ViperKeyCourierSMSFrom, "Ory Kratos") -} - -func (p *Config) CourierSMSEnabled(ctx context.Context) bool { - return p.GetProvider(ctx).Bool(ViperKeyCourierSMSEnabled) + // load legacy configs + channel := CourierChannel{ + ID: "email", + Type: p.CourierEmailStrategy(ctx), + } + if channel.Type == "smtp" { + if err := p.GetProvider(ctx).Koanf.Unmarshal(ViperKeyCourierSMTP, &channel.SMTPConfig); err != nil { + return nil, errors.WithStack(err) + } + } else { + var err error + channel.RequestConfig, err = json.Marshal(p.GetProvider(ctx).Get(ViperKeyCourierHTTPRequestConfig)) + if err != nil { + return nil, errors.WithStack(err) + } + } + return []*CourierChannel{&channel}, nil } func splitUrlAndFragment(s string) (string, string) { diff --git a/driver/config/config_test.go b/driver/config/config_test.go index 602a4971a74e..38e3a01fd055 100644 --- a/driver/config/config_test.go +++ b/driver/config/config_test.go @@ -1149,53 +1149,47 @@ func TestCourierEmailHTTP(t *testing.T) { }) } -func TestCourierSMS(t *testing.T) { +func TestCourierChannels(t *testing.T) { t.Parallel() ctx := context.Background() - t.Run("case=configs set", func(t *testing.T) { - conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, - configx.WithConfigFiles("stub/.kratos.courier.sms.yaml"), configx.SkipValidation()) - assert.True(t, conf.CourierSMSEnabled(ctx)) - snapshotx.SnapshotTExcept(t, conf.CourierSMSRequestConfig(ctx), nil) - assert.Equal(t, "+49123456789", conf.CourierSMSFrom(ctx)) + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.WithConfigFiles("stub/.kratos.courier.channels.yaml"), configx.SkipValidation()) + + channelConfig, err := conf.CourierChannels(ctx) + require.NoError(t, err) + require.Len(t, channelConfig, 1) + assert.Equal(t, channelConfig[0].ID, "phone") + assert.NotEmpty(t, channelConfig[0].RequestConfig) }) t.Run("case=defaults", func(t *testing.T) { conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) - assert.False(t, conf.CourierSMSEnabled(ctx)) - snapshotx.SnapshotTExcept(t, conf.CourierSMSRequestConfig(ctx), nil) - assert.Equal(t, "Ory Kratos", conf.CourierSMSFrom(ctx)) + channelConfig, err := conf.CourierChannels(ctx) + require.NoError(t, err) + assert.Len(t, channelConfig, 1) + assert.Equal(t, channelConfig[0].ID, "email") + assert.Equal(t, channelConfig[0].Type, "smtp") }) -} - -func TestCourierSMTPUrl(t *testing.T) { - t.Parallel() - ctx := context.Background() - for _, tc := range []string{ - "smtp://a:basdasdasda%2Fc@email-smtp.eu-west-3.amazonaws.com:587/", - "smtp://a:b$c@email-smtp.eu-west-3.amazonaws.com:587/", - "smtp://a/a:bc@email-smtp.eu-west-3.amazonaws.com:587", - "smtp://aa:b+c@email-smtp.eu-west-3.amazonaws.com:587/", - "smtp://user?name:password@email-smtp.eu-west-3.amazonaws.com:587/", - "smtp://username:pass%2Fword@email-smtp.eu-west-3.amazonaws.com:587/", - } { - t.Run("case="+tc, func(t *testing.T) { - conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.WithValue(config.ViperKeyCourierSMTPURL, tc), configx.SkipValidation()) - require.NoError(t, err) - parsed, err := conf.CourierSMTPURL(ctx) - require.NoError(t, err) - assert.Equal(t, tc, parsed.String()) - }) - } - - t.Run("invalid", func(t *testing.T) { - conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.WithValue(config.ViperKeyCourierSMTPURL, "smtp://a:b/c@email-smtp.eu-west-3.amazonaws.com:587/"), configx.SkipValidation()) - require.NoError(t, err) - _, err = conf.CourierSMTPURL(ctx) - require.Error(t, err) + t.Run("smtp urls", func(t *testing.T) { + for _, tc := range []string{ + "smtp://a:basdasdasda%2Fc@email-smtp.eu-west-3.amazonaws.com:587/", + "smtp://a:b$c@email-smtp.eu-west-3.amazonaws.com:587/", + "smtp://a/a:bc@email-smtp.eu-west-3.amazonaws.com:587", + "smtp://aa:b+c@email-smtp.eu-west-3.amazonaws.com:587/", + "smtp://user?name:password@email-smtp.eu-west-3.amazonaws.com:587/", + "smtp://username:pass%2Fword@email-smtp.eu-west-3.amazonaws.com:587/", + } { + t.Run("case="+tc, func(t *testing.T) { + conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.WithValue(config.ViperKeyCourierSMTPURL, tc), configx.SkipValidation()) + require.NoError(t, err) + cs, err := conf.CourierChannels(ctx) + require.NoError(t, err) + require.Len(t, cs, 1) + assert.Equal(t, tc, cs[0].SMTPConfig.ConnectionURI) + }) + } }) } @@ -1311,10 +1305,10 @@ func TestCourierTemplatesConfig(t *testing.T) { Subject: "", } - assert.Equal(t, courierTemplateConfig, c.CourierTemplatesHelper(ctx, config.ViperKeyCourierTemplatesVerificationInvalidEmail)) - assert.Equal(t, courierTemplateConfig, c.CourierTemplatesHelper(ctx, config.ViperKeyCourierTemplatesVerificationValidEmail)) + assert.Equal(t, courierTemplateConfig, c.CourierEmailTemplatesHelper(ctx, config.ViperKeyCourierTemplatesVerificationInvalidEmail)) + assert.Equal(t, courierTemplateConfig, c.CourierEmailTemplatesHelper(ctx, config.ViperKeyCourierTemplatesVerificationValidEmail)) // this should return an empty courierEmailTemplate as the key does not exist - assert.Equal(t, courierTemplateConfig, c.CourierTemplatesHelper(ctx, "a_random_key")) + assert.Equal(t, courierTemplateConfig, c.CourierEmailTemplatesHelper(ctx, "a_random_key")) courierTemplateConfig = &config.CourierEmailTemplate{ Body: &config.CourierEmailBodyTemplate{ @@ -1323,7 +1317,7 @@ func TestCourierTemplatesConfig(t *testing.T) { }, Subject: "base64://QWNjb3VudCBBY2Nlc3MgQXR0ZW1wdGVk", } - assert.Equal(t, courierTemplateConfig, c.CourierTemplatesHelper(ctx, config.ViperKeyCourierTemplatesRecoveryInvalidEmail)) + assert.Equal(t, courierTemplateConfig, c.CourierEmailTemplatesHelper(ctx, config.ViperKeyCourierTemplatesRecoveryInvalidEmail)) courierTemplateConfig = &config.CourierEmailTemplate{ Body: &config.CourierEmailBodyTemplate{ @@ -1332,7 +1326,7 @@ func TestCourierTemplatesConfig(t *testing.T) { }, Subject: "base64://UmVjb3ZlciBhY2Nlc3MgdG8geW91ciBhY2NvdW50", } - assert.Equal(t, courierTemplateConfig, c.CourierTemplatesHelper(ctx, config.ViperKeyCourierTemplatesRecoveryValidEmail)) + assert.Equal(t, courierTemplateConfig, c.CourierEmailTemplatesHelper(ctx, config.ViperKeyCourierTemplatesRecoveryValidEmail)) }) } diff --git a/driver/config/stub/.kratos.courier.channels.yaml b/driver/config/stub/.kratos.courier.channels.yaml new file mode 100644 index 000000000000..9f4cdcd6de94 --- /dev/null +++ b/driver/config/stub/.kratos.courier.channels.yaml @@ -0,0 +1,14 @@ +courier: + channels: + - id: phone + request_config: + url: https://ory.sh + method: GET + body: base64://ZnVuY3Rpb24oY3R4KSB7CkJvZHk6IGN0eC5ib2R5LApUbzogY3R4LnRvLEZyb206IGN0eC5mcm9tCn0= + headers: + Content-Type: application/x-www-form-urlencoded + auth: + type: basic_auth + config: + user: ABC + password: DEF diff --git a/embedx/config.schema.json b/embedx/config.schema.json index b8d767ef179a..a42568389fe7 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -1034,12 +1034,36 @@ "properties": { "email": { "$ref": "#/definitions/emailCourierTemplate" + }, + "sms": { + "$ref": "#/definitions/smsCourierTemplate" } }, "required": ["email"] } } }, + "smsCourierTemplate": { + "additionalProperties": false, + "type": "object", + "properties": { + "body": { + "additionalProperties": false, + "type": "object", + "properties": { + "plaintext": { + "type": "string", + "description": "A template send to the SMS provider.", + "format": "uri", + "examples": [ + "file://path/to/body.plaintext.gotmpl", + "https://foo.bar.com/path/to/body.plaintext.gotmpl" + ] + } + } + } + } + }, "emailCourierTemplate": { "additionalProperties": false, "type": "object", @@ -1963,9 +1987,35 @@ } }, "additionalProperties": false + }, + "channels": { + "type": "array", + "items": { + "title": "Courier channel configuration", + "type": "object", + "properties": { + "id": { + "type": "string", + "title": "Channel id", + "description": "The channel id. Corresponds to the .via property of the identity schema for recovery, verification, etc. Currently only phone is supported.", + "maxLength": 32, + "enum": ["sms"] + }, + "type": { + "type": "string", + "title": "Channel type", + "description": "The channel type. Currently only http is supported.", + "enum": ["http"] + }, + "request_config": { + "$ref": "#/definitions/httpRequestConfig" + } + }, + "required": ["id", "request_config"], + "additionalProperties": false + } } }, - "required": ["smtp"], "additionalProperties": false }, "oauth2_provider": { diff --git a/embedx/embedx_test.go b/embedx/embedx_test.go index 27bf4a103a28..c06410f64873 100644 --- a/embedx/embedx_test.go +++ b/embedx/embedx_test.go @@ -5,15 +5,19 @@ package embedx import ( "context" + "embed" + "io/fs" + "strings" "testing" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/assert" "github.com/ory/jsonschema/v3" ) func TestAddSchemaResources(t *testing.T) { - for _, tc := range []struct { description string dependencies []SchemaType @@ -65,7 +69,38 @@ func TestAddSchemaResources(t *testing.T) { assert.NoError(t, err) } } - }) } } + +//go:embed testdata/identity_meta.* +var identityMetaTestCases embed.FS + +func TestIdentityMetaSchema(t *testing.T) { + c := jsonschema.NewCompiler() + err := AddSchemaResources(c, IdentityMeta, IdentityExtension) + require.NoError(t, err) + + schema, err := c.Compile(context.Background(), IdentityMeta.GetSchemaID()) + require.NoError(t, err) + + require.NoError(t, fs.WalkDir(identityMetaTestCases, ".", func(path string, d fs.DirEntry, err error) error { + if d.IsDir() { + return nil + } + + t.Run("case="+path, func(t *testing.T) { + f, err := identityMetaTestCases.Open(path) + require.NoError(t, err) + + err = schema.Validate(f) + if strings.HasSuffix(path, "invalid.schema.json") { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + + return nil + })) +} diff --git a/embedx/identity_extension.schema.json b/embedx/identity_extension.schema.json index 9af2e97f07ce..c105cbe42f95 100644 --- a/embedx/identity_extension.schema.json +++ b/embedx/identity_extension.schema.json @@ -60,7 +60,7 @@ "properties": { "via": { "type": "string", - "enum": ["email", "phone"] + "enum": ["email", "sms"] } } }, diff --git a/embedx/identity_meta.schema.json b/embedx/identity_meta.schema.json index e1213d4893f1..27c2c8eefc2d 100644 --- a/embedx/identity_meta.schema.json +++ b/embedx/identity_meta.schema.json @@ -10,9 +10,7 @@ "properties": { "properties": { "type": "object", - "required": [ - "traits" - ], + "required": ["traits"], "properties": { "traits": { "type": "object", @@ -27,6 +25,32 @@ "patternProperties": { ".*": { "type": "object", + "if": { + "properties": { + "ory.sh/kratos": { + "type": "object", + "properties": { + "verification": {} + }, + "required": ["verification"] + } + }, + "required": ["ory.sh/kratos"] + }, + "then": { + "properties": { + "format": { + "enum": [ + "email", + "tel", + "date", + "time", + "date-time", + "no-validate" + ] + } + } + }, "allOf": [ { "$ref": "ory://identity-extension" @@ -40,9 +64,7 @@ } } }, - "required": [ - "properties" - ] + "required": ["properties"] } ] } diff --git a/embedx/testdata/identity_meta.no_traits.invalid.schema.json b/embedx/testdata/identity_meta.no_traits.invalid.schema.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/embedx/testdata/identity_meta.no_traits.invalid.schema.json @@ -0,0 +1 @@ +{} diff --git a/embedx/testdata/identity_meta.simple.valid.schema.json b/embedx/testdata/identity_meta.simple.valid.schema.json new file mode 100644 index 000000000000..6f090b383a55 --- /dev/null +++ b/embedx/testdata/identity_meta.simple.valid.schema.json @@ -0,0 +1,23 @@ +{ + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + }, + "verification": { + "via": "email" + } + } + } + } + } + } +} diff --git a/embedx/testdata/identity_meta.verification_format.invalid.schema.json b/embedx/testdata/identity_meta.verification_format.invalid.schema.json new file mode 100644 index 000000000000..1dc46bdcfdc3 --- /dev/null +++ b/embedx/testdata/identity_meta.verification_format.invalid.schema.json @@ -0,0 +1,23 @@ +{ + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "unknown", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + }, + "verification": { + "via": "email" + } + } + } + } + } + } +} diff --git a/go.mod b/go.mod index 93496e8b4c93..7232373c704c 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ replace ( ) require ( + code.dny.dev/ssrf v0.2.0 // indirect github.com/Masterminds/sprig/v3 v3.2.3 github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 github.com/avast/retry-go/v3 v3.1.1 @@ -75,7 +76,7 @@ require ( github.com/ory/jsonschema/v3 v3.0.8 github.com/ory/mail/v3 v3.0.0 github.com/ory/nosurf v1.2.7 - github.com/ory/x v0.0.604 + github.com/ory/x v0.0.607 github.com/peterhellberg/link v1.2.0 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 github.com/pkg/errors v0.9.1 @@ -93,10 +94,11 @@ require ( github.com/tidwall/sjson v1.2.5 github.com/urfave/negroni v1.0.0 github.com/zmb3/spotify/v2 v2.4.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 - go.opentelemetry.io/otel v1.19.0 - go.opentelemetry.io/otel/trace v1.19.0 - golang.org/x/crypto v0.15.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 + go.opentelemetry.io/otel v1.21.0 + go.opentelemetry.io/otel/sdk v1.21.0 + go.opentelemetry.io/otel/trace v1.21.0 + golang.org/x/crypto v0.17.0 golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa golang.org/x/net v0.18.0 golang.org/x/oauth2 v0.14.0 @@ -106,10 +108,6 @@ require ( google.golang.org/grpc v1.59.0 ) -require go.opentelemetry.io/otel/sdk v1.19.0 - -require code.dny.dev/ssrf v0.2.0 // indirect - require ( github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect @@ -148,7 +146,7 @@ require ( github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/fxamacker/cbor/v2 v2.4.0 // indirect github.com/go-crypt/x v0.2.1 // indirect - github.com/go-jose/go-jose/v3 v3.0.0 // indirect + github.com/go-jose/go-jose/v3 v3.0.1 // indirect github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.21.4 // indirect @@ -299,19 +297,19 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect go.mongodb.org/mongo-driver v1.11.3 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.45.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 // indirect go.opentelemetry.io/contrib/propagators/b3 v1.20.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.20.0 // indirect - go.opentelemetry.io/contrib/samplers/jaegerremote v0.14.0 // indirect + go.opentelemetry.io/contrib/samplers/jaegerremote v0.15.1 // indirect go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect; / indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect; / indirect - go.opentelemetry.io/otel/exporters/zipkin v1.19.0 // indirect; / indirect - go.opentelemetry.io/otel/metric v1.19.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect; / indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 // indirect; / indirect + go.opentelemetry.io/otel/exporters/zipkin v1.21.0 // indirect; / indirect + go.opentelemetry.io/otel/metric v1.21.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect golang.org/x/mod v0.14.0 // indirect - golang.org/x/sys v0.14.0 // indirect - golang.org/x/term v0.14.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/term v0.15.0 // indirect golang.org/x/tools v0.15.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/appengine v1.6.8 // indirect diff --git a/go.sum b/go.sum index e22a551b44c4..bc1e37527262 100644 --- a/go.sum +++ b/go.sum @@ -215,8 +215,8 @@ github.com/go-faker/faker/v4 v4.2.0/go.mod h1:F/bBy8GH9NxOxMInug5Gx4WYeG6fHJZ8Ol github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= -github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= +github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= +github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -840,8 +840,8 @@ github.com/ory/nosurf v1.2.7 h1:YrHrbSensQyU6r6HT/V5+HPdVEgrOTMJiLoJABSBOp4= github.com/ory/nosurf v1.2.7/go.mod h1:d4L3ZBa7Amv55bqxCBtCs63wSlyaiCkWVl4vKf3OUxA= github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2 h1:zm6sDvHy/U9XrGpixwHiuAwpp0Ock6khSVHkrv6lQQU= github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/ory/x v0.0.604 h1:I02tf+FIEcA98+tPyaspRQYxlX28uIGAB1g7JTh8GUk= -github.com/ory/x v0.0.604/go.mod h1:AFyMDGw6bh14PAGQITzlFuF/1OAvEXOX61PYbxJyeS8= +github.com/ory/x v0.0.607 h1:qNP1gU6RWVtsEB04rPht+1rV2DqQhvOAN2sF+4eqVWo= +github.com/ory/x v0.0.607/go.mod h1:fCYvVVHo8wYrCwLyU8+9hFY3IRo4EZM3KI30ysDsDYY= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -1069,32 +1069,32 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.45.0 h1:2ea0IkZBsWH+HA2GkD+7+hRw2u97jzdFyRtXuO14a1s= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.45.0/go.mod h1:4m3RnBBb+7dB9d21y510oO1pdB1V4J6smNf14WXcBFQ= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 h1:x8Z78aZx8cOF0+Kkazoc7lwUNMGy0LrzEMxTm4BbTxg= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 h1:gbhw/u49SS3gkPWiYweQNJGm/uJN5GkI/FrosxSHT7A= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1/go.mod h1:GnOaBaFQ2we3b9AGWJpsBa7v1S5RlQzlC3O7dRMxZhM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= go.opentelemetry.io/contrib/propagators/b3 v1.20.0 h1:Yty9Vs4F3D6/liF1o6FNt0PvN85h/BJJ6DQKJ3nrcM0= go.opentelemetry.io/contrib/propagators/b3 v1.20.0/go.mod h1:On4VgbkqYL18kbJlWsa18+cMNe6rYpBnPi1ARI/BrsU= go.opentelemetry.io/contrib/propagators/jaeger v1.20.0 h1:iVhNKkMIpzyZqxk8jkDU2n4DFTD+FbpGacvooxEvyyc= go.opentelemetry.io/contrib/propagators/jaeger v1.20.0/go.mod h1:cpSABr0cm/AH/HhbJjn+AudBVUMgZWdfN3Gb+ZqxSZc= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.14.0 h1:Xg9iU9DF9V9zC6NI8sJthYqHlSWsWAQMTXM8QIErKlc= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.14.0/go.mod h1:ExRuq62/gYluX5fzTTZif5WujyG51ail4APTbBUu+S4= -go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= -go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.15.1 h1:Qb+5A+JbIjXwO7l4HkRUhgIn4Bzz0GNS2q+qdmSx+0c= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.15.1/go.mod h1:G4vNCm7fRk0kjZ6pGNLo5SpLxAUvOfSrcaegnT8TPck= +go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= -go.opentelemetry.io/otel/exporters/zipkin v1.19.0 h1:EGY0h5mGliP9o/nIkVuLI0vRiQqmsYOcbwCuotksO1o= -go.opentelemetry.io/otel/exporters/zipkin v1.19.0/go.mod h1:JQgTGJP11yi3o4GHzIWYodhPisxANdqxF1eHwDSnJrI= -go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= -go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= -go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= -go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= -go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= -go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkEZCJWobwBqMwC0cwCq8/wkkRy/OowZg5OArWZrM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I= +go.opentelemetry.io/otel/exporters/zipkin v1.21.0 h1:D+Gv6lSfrFBWmQYyxKjDd0Zuld9SRXpIrEsKZvE4DO4= +go.opentelemetry.io/otel/exporters/zipkin v1.21.0/go.mod h1:83oMKR6DzmHisFOW3I+yIMGZUTjxiWaiBI8M8+TU5zE= +go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= +go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= +go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -1136,8 +1136,8 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1354,8 +1354,8 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20191110171634-ad39bd3f0407/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1367,8 +1367,8 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= -golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/identity/address.go b/identity/address.go index 4c4145114083..2e9175642e84 100644 --- a/identity/address.go +++ b/identity/address.go @@ -5,5 +5,4 @@ package identity const ( AddressTypeEmail = "email" - AddressTypePhone = "phone" ) diff --git a/identity/credentials.go b/identity/credentials.go index c323f7dcc790..4f2dbd522874 100644 --- a/identity/credentials.go +++ b/identity/credentials.go @@ -146,15 +146,6 @@ func ParseCredentialsType(in string) (CredentialsType, bool) { return "", false } -// swagger:ignore -type CredentialsIdentifierAddressType string - -const ( - CredentialsIdentifierAddressTypeEmail CredentialsIdentifierAddressType = AddressTypeEmail - CredentialsIdentifierAddressTypePhone CredentialsIdentifierAddressType = AddressTypePhone - CredentialsIdentifierAddressTypeNone CredentialsIdentifierAddressType = "none" -) - // Credentials represents a specific credential type // // swagger:model identityCredentials diff --git a/identity/credentials_code.go b/identity/credentials_code.go index 184479ae1700..abc9decbbf46 100644 --- a/identity/credentials_code.go +++ b/identity/credentials_code.go @@ -11,7 +11,6 @@ type CodeAddressType string const ( CodeAddressTypeEmail CodeAddressType = AddressTypeEmail - CodeAddressTypePhone CodeAddressType = AddressTypePhone ) // CredentialsCode represents a one time login/registration code diff --git a/identity/extension_credentials.go b/identity/extension_credentials.go index 69fb53810fbc..3baa826b2e9c 100644 --- a/identity/extension_credentials.go +++ b/identity/extension_credentials.go @@ -26,7 +26,7 @@ func NewSchemaExtensionCredentials(i *Identity) *SchemaExtensionCredentials { return &SchemaExtensionCredentials{i: i} } -func (r *SchemaExtensionCredentials) setIdentifier(ct CredentialsType, value interface{}, addressType CredentialsIdentifierAddressType) { +func (r *SchemaExtensionCredentials) setIdentifier(ct CredentialsType, value interface{}) { cred, ok := r.i.GetCredentials(ct) if !ok { cred = &Credentials{ @@ -49,11 +49,11 @@ func (r *SchemaExtensionCredentials) Run(ctx jsonschema.ValidationContext, s sch defer r.l.Unlock() if s.Credentials.Password.Identifier { - r.setIdentifier(CredentialsTypePassword, value, CredentialsIdentifierAddressTypeNone) + r.setIdentifier(CredentialsTypePassword, value) } if s.Credentials.WebAuthn.Identifier { - r.setIdentifier(CredentialsTypeWebAuthn, value, CredentialsIdentifierAddressTypeNone) + r.setIdentifier(CredentialsTypeWebAuthn, value) } if s.Credentials.Code.Identifier { @@ -63,13 +63,13 @@ func (r *SchemaExtensionCredentials) Run(ctx jsonschema.ValidationContext, s sch return ctx.Error("format", "%q is not a valid %q", value, s.Credentials.Code.Via) } - r.setIdentifier(CredentialsTypeCodeAuth, value, AddressTypeEmail) + r.setIdentifier(CredentialsTypeCodeAuth, value) // case f.AddCase(AddressTypePhone): // if !jsonschema.Formats["tel"](value) { // return ctx.Error("format", "%q is not a valid %q", value, s.Credentials.Code.Via) // } - // - // r.setIdentifier(CredentialsTypeCodeAuth, value, CredentialsIdentifierAddressType(AddressTypePhone)) + + // r.setIdentifier(CredentialsTypeCodeAuth, value, CredentialsIdentifierAddressTypePhone) default: return ctx.Error("", "credentials.code.via has unknown value %q", s.Credentials.Code.Via) } diff --git a/identity/extension_verify.go b/identity/extension_verification.go similarity index 56% rename from identity/extension_verify.go rename to identity/extension_verification.go index 8fa86c0754a7..3b3f92581c37 100644 --- a/identity/extension_verify.go +++ b/identity/extension_verification.go @@ -9,10 +9,18 @@ import ( "sync" "time" + "golang.org/x/exp/maps" + "github.com/ory/jsonschema/v3" "github.com/ory/kratos/schema" ) +func init() { + jsonschema.Formats["no-validate"] = func(v interface{}) bool { + return true + } +} + type SchemaExtensionVerification struct { lifespan time.Duration l sync.Mutex @@ -24,40 +32,57 @@ func NewSchemaExtensionVerification(i *Identity, lifespan time.Duration) *Schema return &SchemaExtensionVerification{i: i, lifespan: lifespan} } +const ( + ChannelTypeEmail = "email" + ChannelTypeSMS = "sms" +) + func (r *SchemaExtensionVerification) Run(ctx jsonschema.ValidationContext, s schema.ExtensionConfig, value interface{}) error { r.l.Lock() defer r.l.Unlock() - switch s.Verification.Via { - case AddressTypeEmail: - if !jsonschema.Formats["email"](value) { - return ctx.Error("format", "%q is not valid %q", value, "email") - } - - address := NewVerifiableEmailAddress( - strings.ToLower(strings.TrimSpace( - fmt.Sprintf("%s", value))), r.i.ID) - - r.appendAddress(address) + if s.Verification.Via == "" { + return nil + } + format, ok := s.RawSchema["format"] + if !ok { + format = "" + } + formatString, ok := format.(string) + if !ok { return nil + } - case AddressTypePhone: - if !jsonschema.Formats["tel"](value) { - return ctx.Error("format", "%q is not valid %q", value, "phone") + if formatString == "" { + switch s.Verification.Via { + case ChannelTypeEmail: + formatString = "email" + formatter, ok := jsonschema.Formats[formatString] + if !ok { + supportedKeys := maps.Keys(jsonschema.Formats) + return ctx.Error("format", "format %q is not supported. Supported formats are [%s]", formatString, strings.Join(supportedKeys, ", ")) + } + + if !formatter(value) { + return ctx.Error("format", "%q is not valid %q", value, formatString) + } + default: + return ctx.Error("format", "no format specified. A format is required if verification is enabled. If this was intentional, please set \"format\" to \"no-validate\"") } + } - address := NewVerifiablePhoneAddress(fmt.Sprintf("%s", value), r.i.ID) - - r.appendAddress(address) - - return nil - - case "": - return nil + var normalized string + switch formatString { + case "email": + normalized = strings.ToLower(strings.TrimSpace(fmt.Sprintf("%s", value))) + default: + normalized = strings.TrimSpace(fmt.Sprintf("%s", value)) } - return ctx.Error("", "verification.via has unknown value %q", s.Verification.Via) + address := NewVerifiableAddress(normalized, r.i.ID, s.Verification.Via) + r.appendAddress(address) + return nil } func (r *SchemaExtensionVerification) Finish() error { diff --git a/identity/extension_verify_test.go b/identity/extension_verification_test.go similarity index 83% rename from identity/extension_verify_test.go rename to identity/extension_verification_test.go index 9085f2e03ea5..ebf2c09f207e 100644 --- a/identity/extension_verify_test.go +++ b/identity/extension_verification_test.go @@ -6,12 +6,13 @@ package identity import ( "bytes" "context" - "errors" "fmt" + "net" "reflect" "testing" "time" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -22,13 +23,17 @@ import ( ) const ( - emailSchemaPath = "file://./stub/extension/verify/email.schema.json" - phoneSchemaPath = "file://./stub/extension/verify/phone.schema.json" + emailSchemaPath = "file://./stub/extension/verify/email.schema.json" + phoneSchemaPath = "file://./stub/extension/verify/phone.schema.json" + missingFormatSchemaPath = "file://./stub/extension/verify/missing-format.schema.json" + legacyEmailMissingFormatSchemaPath = "file://./stub/extension/verify/legacy-email-missing-format.schema.json" + noValidateSchemaPath = "file://./stub/extension/verify/no-validate.schema.json" ) var ctx = context.Background() func TestSchemaExtensionVerification(t *testing.T) { + net.IP{}.IsPrivate() t.Run("address verification", func(t *testing.T) { iid := x.NewUUID() @@ -194,7 +199,7 @@ func TestSchemaExtensionVerification(t *testing.T) { Value: "+18004444444", Verified: false, Status: VerifiableAddressStatusPending, - Via: VerifiableAddressTypePhone, + Via: ChannelTypeSMS, IdentityID: iid, }, }, @@ -208,7 +213,7 @@ func TestSchemaExtensionVerification(t *testing.T) { Value: "+442087599036", Verified: false, Status: VerifiableAddressStatusPending, - Via: VerifiableAddressTypePhone, + Via: ChannelTypeSMS, IdentityID: iid, }, }, @@ -217,7 +222,7 @@ func TestSchemaExtensionVerification(t *testing.T) { Value: "+18004444444", Verified: false, Status: VerifiableAddressStatusPending, - Via: VerifiableAddressTypePhone, + Via: ChannelTypeSMS, IdentityID: iid, }, }, @@ -231,14 +236,14 @@ func TestSchemaExtensionVerification(t *testing.T) { Value: "+442087599036", Verified: true, Status: VerifiableAddressStatusCompleted, - Via: VerifiableAddressTypePhone, + Via: ChannelTypeSMS, IdentityID: iid, }, { Value: "+380634872774", Verified: true, Status: VerifiableAddressStatusCompleted, - Via: VerifiableAddressTypePhone, + Via: ChannelTypeSMS, IdentityID: iid, }, }, @@ -247,14 +252,14 @@ func TestSchemaExtensionVerification(t *testing.T) { Value: "+442087599036", Verified: true, Status: VerifiableAddressStatusCompleted, - Via: VerifiableAddressTypePhone, + Via: ChannelTypeSMS, IdentityID: iid, }, { Value: "+18004444444", Verified: false, Status: VerifiableAddressStatusPending, - Via: VerifiableAddressTypePhone, + Via: ChannelTypeSMS, IdentityID: iid, }, }, @@ -268,14 +273,14 @@ func TestSchemaExtensionVerification(t *testing.T) { Value: "+18004444444", Verified: false, Status: VerifiableAddressStatusPending, - Via: VerifiableAddressTypePhone, + Via: ChannelTypeSMS, IdentityID: iid, }, { Value: "+380634872774", Verified: true, Status: VerifiableAddressStatusCompleted, - Via: VerifiableAddressTypePhone, + Via: ChannelTypeSMS, IdentityID: iid, }, }, @@ -284,14 +289,14 @@ func TestSchemaExtensionVerification(t *testing.T) { Value: "+18004444444", Verified: false, Status: VerifiableAddressStatusPending, - Via: VerifiableAddressTypePhone, + Via: ChannelTypeSMS, IdentityID: iid, }, { Value: "+442087599036", Verified: false, Status: VerifiableAddressStatusPending, - Via: VerifiableAddressTypePhone, + Via: ChannelTypeSMS, IdentityID: iid, }, }, @@ -305,21 +310,21 @@ func TestSchemaExtensionVerification(t *testing.T) { Value: "+18004444444", Verified: false, Status: VerifiableAddressStatusPending, - Via: VerifiableAddressTypePhone, + Via: ChannelTypeSMS, IdentityID: iid, }, { Value: "+442087599036", Verified: false, Status: VerifiableAddressStatusPending, - Via: VerifiableAddressTypePhone, + Via: ChannelTypeSMS, IdentityID: iid, }, { Value: "+380634872774", Verified: false, Status: VerifiableAddressStatusPending, - Via: VerifiableAddressTypePhone, + Via: ChannelTypeSMS, IdentityID: iid, }, }, @@ -328,7 +333,41 @@ func TestSchemaExtensionVerification(t *testing.T) { name: "phone:must return error for malformed input", schema: phoneSchemaPath, doc: `{"phones":["+18004444444","+18004444444","12112112"], "username": "+380634872774"}`, - expectErr: errors.New("I[#/phones/2] S[#/properties/phones/items/format] \"12112112\" is not valid \"phone\""), + expectErr: errors.New("I[#/phones/2] S[#/properties/phones/items/format] \"12112112\" is not valid \"tel\""), + }, + { + name: "missing format returns an error", + schema: missingFormatSchemaPath, + doc: `{"phone": "+380634872774"}`, + expectErr: errors.New("I[#/phone] S[#/properties/phone/format] no format specified. A format is required if verification is enabled. If this was intentional, please set \"format\" to \"no-validate\""), + }, + { + name: "missing format works for email if format is missing", + schema: legacyEmailMissingFormatSchemaPath, + doc: `{"email": "user@ory.sh"}`, + expect: []VerifiableAddress{ + { + Value: "user@ory.sh", + Verified: false, + Status: VerifiableAddressStatusPending, + Via: ChannelTypeEmail, + IdentityID: iid, + }, + }, + }, + { + name: "format: no-validate works", + schema: noValidateSchemaPath, + doc: `{"phone": "not a phone number"}`, + expect: []VerifiableAddress{ + { + Value: "not a phone number", + Verified: false, + Status: VerifiableAddressStatusPending, + Via: ChannelTypeSMS, + IdentityID: iid, + }, + }, }, } { t.Run(fmt.Sprintf("case=%v", tc.name), func(t *testing.T) { @@ -357,7 +396,6 @@ func TestSchemaExtensionVerification(t *testing.T) { }) } }) - } func mustContainAddress(t *testing.T, expected, actual []VerifiableAddress) { diff --git a/identity/handler.go b/identity/handler.go index 9eb5f11cba99..de50cb73eb2f 100644 --- a/identity/handler.go +++ b/identity/handler.go @@ -138,12 +138,12 @@ type listIdentitiesResponse struct { type listIdentitiesParameters struct { migrationpagination.RequestParameters - // IdsFilter is list of ids used to filter identities. + // List of ids used to filter identities. // If this list is empty, then no filter will be applied. // // required: false // in: query - IdsFilter []string `json:"ids_filter"` + IdsFilter []string `json:"ids"` // CredentialsIdentifier is the identifier (username, email) of the credentials to look up using exact match. // Only one of CredentialsIdentifier and CredentialsIdentifierSimilar can be used. diff --git a/identity/identity_verification.go b/identity/identity_verification.go index 697af0594b4f..54ac435ec9fd 100644 --- a/identity/identity_verification.go +++ b/identity/identity_verification.go @@ -15,7 +15,6 @@ import ( const ( VerifiableAddressTypeEmail VerifiableAddressType = AddressTypeEmail - VerifiableAddressTypePhone VerifiableAddressType = AddressTypePhone VerifiableAddressStatusPending VerifiableAddressStatus = "pending" VerifiableAddressStatusSent VerifiableAddressStatus = "sent" @@ -25,7 +24,7 @@ const ( // VerifiableAddressType must not exceed 16 characters as that is the limitation in the SQL Schema // // swagger:model identityVerifiableAddressType -type VerifiableAddressType string +type VerifiableAddressType = string // VerifiableAddressStatus must not exceed 16 characters as that is the limitation in the SQL Schema // @@ -54,14 +53,14 @@ type VerifiableAddress struct { // The delivery method // - // enum: ["email"] + // enum: email,sms // example: email // required: true - Via VerifiableAddressType `json:"via" db:"via"` + Via string `json:"via" db:"via"` // The verified address status // - // enum: ["pending","sent","completed"] + // enum: pending,sent,completed // example: sent // required: true Status VerifiableAddressStatus `json:"status" db:"status"` @@ -87,36 +86,20 @@ type VerifiableAddress struct { NID uuid.UUID `json:"-" faker:"-" db:"nid"` } -func (v VerifiableAddressType) HTMLFormInputType() string { - switch v { - case VerifiableAddressTypeEmail: - return "email" - case VerifiableAddressTypePhone: - return "phone" - } - return "" -} - func (a VerifiableAddress) TableName(ctx context.Context) string { return "identity_verifiable_addresses" } func NewVerifiableEmailAddress(value string, identity uuid.UUID) *VerifiableAddress { - return &VerifiableAddress{ - Value: value, - Verified: false, - Status: VerifiableAddressStatusPending, - Via: VerifiableAddressTypeEmail, - IdentityID: identity, - } + return NewVerifiableAddress(value, identity, VerifiableAddressTypeEmail) } -func NewVerifiablePhoneAddress(value string, identity uuid.UUID) *VerifiableAddress { +func NewVerifiableAddress(value string, identity uuid.UUID, channel string) *VerifiableAddress { return &VerifiableAddress{ Value: value, Verified: false, Status: VerifiableAddressStatusPending, - Via: VerifiableAddressTypePhone, + Via: channel, IdentityID: identity, } } diff --git a/identity/pool.go b/identity/pool.go index 1da5181b796a..5316f8a53ff9 100644 --- a/identity/pool.go +++ b/identity/pool.go @@ -39,7 +39,7 @@ type ( GetIdentity(context.Context, uuid.UUID, sqlxx.Expandables) (*Identity, error) // FindVerifiableAddressByValue returns a matching address or sql.ErrNoRows if no address could be found. - FindVerifiableAddressByValue(ctx context.Context, via VerifiableAddressType, address string) (*VerifiableAddress, error) + FindVerifiableAddressByValue(ctx context.Context, via string, address string) (*VerifiableAddress, error) // FindRecoveryAddressByValue returns a matching address or sql.ErrNoRows if no address could be found. FindRecoveryAddressByValue(ctx context.Context, via RecoveryAddressType, address string) (*RecoveryAddress, error) diff --git a/identity/stub/extension/verify/legacy-email-missing-format.schema.json b/identity/stub/extension/verify/legacy-email-missing-format.schema.json new file mode 100644 index 000000000000..50d7c5f7b548 --- /dev/null +++ b/identity/stub/extension/verify/legacy-email-missing-format.schema.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "properties": { + "email": { + "type": "string", + "ory.sh/kratos": { + "verification": { + "via": "email" + } + } + } + } +} diff --git a/identity/stub/extension/verify/missing-format.schema.json b/identity/stub/extension/verify/missing-format.schema.json new file mode 100644 index 000000000000..b3716126bb0d --- /dev/null +++ b/identity/stub/extension/verify/missing-format.schema.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "properties": { + "phone": { + "type": "string", + "ory.sh/kratos": { + "verification": { + "via": "sms" + } + } + } + } +} diff --git a/identity/stub/extension/verify/no-validate.schema.json b/identity/stub/extension/verify/no-validate.schema.json new file mode 100644 index 000000000000..d92bb04ef4c4 --- /dev/null +++ b/identity/stub/extension/verify/no-validate.schema.json @@ -0,0 +1,14 @@ +{ + "type": "object", + "properties": { + "phone": { + "type": "string", + "format": "noformat", + "ory.sh/kratos": { + "verification": { + "via": "sms" + } + } + } + } +} diff --git a/identity/stub/extension/verify/phone.schema.json b/identity/stub/extension/verify/phone.schema.json index 2bd7d6f4fa35..6b273dccf880 100644 --- a/identity/stub/extension/verify/phone.schema.json +++ b/identity/stub/extension/verify/phone.schema.json @@ -5,18 +5,20 @@ "type": "array", "items": { "type": "string", + "format": "tel", "ory.sh/kratos": { "verification": { - "via": "phone" + "via": "sms" } } } }, "username": { "type": "string", + "format": "tel", "ory.sh/kratos": { "verification": { - "via": "phone" + "via": "sms" } } } diff --git a/identity/validator.go b/identity/validator.go index 878162e0bd7b..3bd8f9476fa5 100644 --- a/identity/validator.go +++ b/identity/validator.go @@ -35,8 +35,8 @@ func NewValidator(d validatorDependencies) *Validator { return &Validator{v: schema.NewValidator(), d: d} } -func (v *Validator) ValidateWithRunner(ctx context.Context, i *Identity, runners ...schema.Extension) error { - runner, err := schema.NewExtensionRunner(ctx, runners...) +func (v *Validator) ValidateWithRunner(ctx context.Context, i *Identity, runners ...schema.ValidateExtension) error { + runner, err := schema.NewExtensionRunner(ctx, schema.WithValidateRunners(runners...)) if err != nil { return err } diff --git a/internal/client-go/.openapi-generator/VERSION b/internal/client-go/.openapi-generator/VERSION index 0df17dd0f6a3..4b49d9bb63ee 100644 --- a/internal/client-go/.openapi-generator/VERSION +++ b/internal/client-go/.openapi-generator/VERSION @@ -1 +1 @@ -6.2.1 \ No newline at end of file +7.2.0 \ No newline at end of file diff --git a/internal/client-go/api_identity.go b/internal/client-go/api_identity.go index a88dba49aab8..ca2b697f8cf5 100644 --- a/internal/client-go/api_identity.go +++ b/internal/client-go/api_identity.go @@ -2052,7 +2052,7 @@ type IdentityApiApiListIdentitiesRequest struct { pageSize *int64 pageToken *string consistency *string - idsFilter *[]string + ids *[]string credentialsIdentifier *string previewCredentialsIdentifierSimilar *string } @@ -2077,8 +2077,8 @@ func (r IdentityApiApiListIdentitiesRequest) Consistency(consistency string) Ide r.consistency = &consistency return r } -func (r IdentityApiApiListIdentitiesRequest) IdsFilter(idsFilter []string) IdentityApiApiListIdentitiesRequest { - r.idsFilter = &idsFilter +func (r IdentityApiApiListIdentitiesRequest) Ids(ids []string) IdentityApiApiListIdentitiesRequest { + r.ids = &ids return r } func (r IdentityApiApiListIdentitiesRequest) CredentialsIdentifier(credentialsIdentifier string) IdentityApiApiListIdentitiesRequest { @@ -2147,15 +2147,15 @@ func (a *IdentityApiService) ListIdentitiesExecute(r IdentityApiApiListIdentitie if r.consistency != nil { localVarQueryParams.Add("consistency", parameterToString(*r.consistency, "")) } - if r.idsFilter != nil { - t := *r.idsFilter + if r.ids != nil { + t := *r.ids if reflect.TypeOf(t).Kind() == reflect.Slice { s := reflect.ValueOf(t) for i := 0; i < s.Len(); i++ { - localVarQueryParams.Add("ids_filter", parameterToString(s.Index(i), "multi")) + localVarQueryParams.Add("ids", parameterToString(s.Index(i), "multi")) } } else { - localVarQueryParams.Add("ids_filter", parameterToString(t, "multi")) + localVarQueryParams.Add("ids", parameterToString(t, "multi")) } } if r.credentialsIdentifier != nil { diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 734252e68153..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,8 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/client-go/model_message.go b/internal/client-go/model_message.go index f0452185f169..405575779c78 100644 --- a/internal/client-go/model_message.go +++ b/internal/client-go/model_message.go @@ -18,7 +18,8 @@ import ( // Message struct for Message type Message struct { - Body string `json:"body"` + Body string `json:"body"` + Channel *string `json:"channel,omitempty"` // CreatedAt is a helper struct field for gobuffalo.pop. CreatedAt time.Time `json:"created_at"` // Dispatches store information about the attempts of delivering a message May contain an error if any happened, or just the `success` state. @@ -28,7 +29,7 @@ type Message struct { SendCount int64 `json:"send_count"` Status CourierMessageStatus `json:"status"` Subject string `json:"subject"` - // recovery_invalid TypeRecoveryInvalid recovery_valid TypeRecoveryValid recovery_code_invalid TypeRecoveryCodeInvalid recovery_code_valid TypeRecoveryCodeValid verification_invalid TypeVerificationInvalid verification_valid TypeVerificationValid verification_code_invalid TypeVerificationCodeInvalid verification_code_valid TypeVerificationCodeValid otp TypeOTP stub TypeTestStub login_code_valid TypeLoginCodeValid registration_code_valid TypeRegistrationCodeValid + // recovery_invalid TypeRecoveryInvalid recovery_valid TypeRecoveryValid recovery_code_invalid TypeRecoveryCodeInvalid recovery_code_valid TypeRecoveryCodeValid verification_invalid TypeVerificationInvalid verification_valid TypeVerificationValid verification_code_invalid TypeVerificationCodeInvalid verification_code_valid TypeVerificationCodeValid stub TypeTestStub login_code_valid TypeLoginCodeValid registration_code_valid TypeRegistrationCodeValid TemplateType string `json:"template_type"` Type CourierMessageType `json:"type"` // UpdatedAt is a helper struct field for gobuffalo.pop. @@ -86,6 +87,38 @@ func (o *Message) SetBody(v string) { o.Body = v } +// GetChannel returns the Channel field value if set, zero value otherwise. +func (o *Message) GetChannel() string { + if o == nil || o.Channel == nil { + var ret string + return ret + } + return *o.Channel +} + +// GetChannelOk returns a tuple with the Channel field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Message) GetChannelOk() (*string, bool) { + if o == nil || o.Channel == nil { + return nil, false + } + return o.Channel, true +} + +// HasChannel returns a boolean if a field has been set. +func (o *Message) HasChannel() bool { + if o != nil && o.Channel != nil { + return true + } + + return false +} + +// SetChannel gets a reference to the given string and assigns it to the Channel field. +func (o *Message) SetChannel(v string) { + o.Channel = &v +} + // GetCreatedAt returns the CreatedAt field value func (o *Message) GetCreatedAt() time.Time { if o == nil { @@ -339,6 +372,9 @@ func (o Message) MarshalJSON() ([]byte, error) { if true { toSerialize["body"] = o.Body } + if o.Channel != nil { + toSerialize["channel"] = o.Channel + } if true { toSerialize["created_at"] = o.CreatedAt } diff --git a/internal/client-go/model_verifiable_identity_address.go b/internal/client-go/model_verifiable_identity_address.go index 516a8107e40a..820881b2d3a2 100644 --- a/internal/client-go/model_verifiable_identity_address.go +++ b/internal/client-go/model_verifiable_identity_address.go @@ -31,7 +31,7 @@ type VerifiableIdentityAddress struct { // Indicates if the address has already been verified Verified bool `json:"verified"` VerifiedAt *time.Time `json:"verified_at,omitempty"` - // VerifiableAddressType must not exceed 16 characters as that is the limitation in the SQL Schema + // The delivery method Via string `json:"via"` } diff --git a/internal/httpclient/.openapi-generator/VERSION b/internal/httpclient/.openapi-generator/VERSION index 0df17dd0f6a3..4b49d9bb63ee 100644 --- a/internal/httpclient/.openapi-generator/VERSION +++ b/internal/httpclient/.openapi-generator/VERSION @@ -1 +1 @@ -6.2.1 \ No newline at end of file +7.2.0 \ No newline at end of file diff --git a/internal/httpclient/api_identity.go b/internal/httpclient/api_identity.go index a88dba49aab8..ca2b697f8cf5 100644 --- a/internal/httpclient/api_identity.go +++ b/internal/httpclient/api_identity.go @@ -2052,7 +2052,7 @@ type IdentityApiApiListIdentitiesRequest struct { pageSize *int64 pageToken *string consistency *string - idsFilter *[]string + ids *[]string credentialsIdentifier *string previewCredentialsIdentifierSimilar *string } @@ -2077,8 +2077,8 @@ func (r IdentityApiApiListIdentitiesRequest) Consistency(consistency string) Ide r.consistency = &consistency return r } -func (r IdentityApiApiListIdentitiesRequest) IdsFilter(idsFilter []string) IdentityApiApiListIdentitiesRequest { - r.idsFilter = &idsFilter +func (r IdentityApiApiListIdentitiesRequest) Ids(ids []string) IdentityApiApiListIdentitiesRequest { + r.ids = &ids return r } func (r IdentityApiApiListIdentitiesRequest) CredentialsIdentifier(credentialsIdentifier string) IdentityApiApiListIdentitiesRequest { @@ -2147,15 +2147,15 @@ func (a *IdentityApiService) ListIdentitiesExecute(r IdentityApiApiListIdentitie if r.consistency != nil { localVarQueryParams.Add("consistency", parameterToString(*r.consistency, "")) } - if r.idsFilter != nil { - t := *r.idsFilter + if r.ids != nil { + t := *r.ids if reflect.TypeOf(t).Kind() == reflect.Slice { s := reflect.ValueOf(t) for i := 0; i < s.Len(); i++ { - localVarQueryParams.Add("ids_filter", parameterToString(s.Index(i), "multi")) + localVarQueryParams.Add("ids", parameterToString(s.Index(i), "multi")) } } else { - localVarQueryParams.Add("ids_filter", parameterToString(t, "multi")) + localVarQueryParams.Add("ids", parameterToString(t, "multi")) } } if r.credentialsIdentifier != nil { diff --git a/internal/httpclient/model_message.go b/internal/httpclient/model_message.go index f0452185f169..405575779c78 100644 --- a/internal/httpclient/model_message.go +++ b/internal/httpclient/model_message.go @@ -18,7 +18,8 @@ import ( // Message struct for Message type Message struct { - Body string `json:"body"` + Body string `json:"body"` + Channel *string `json:"channel,omitempty"` // CreatedAt is a helper struct field for gobuffalo.pop. CreatedAt time.Time `json:"created_at"` // Dispatches store information about the attempts of delivering a message May contain an error if any happened, or just the `success` state. @@ -28,7 +29,7 @@ type Message struct { SendCount int64 `json:"send_count"` Status CourierMessageStatus `json:"status"` Subject string `json:"subject"` - // recovery_invalid TypeRecoveryInvalid recovery_valid TypeRecoveryValid recovery_code_invalid TypeRecoveryCodeInvalid recovery_code_valid TypeRecoveryCodeValid verification_invalid TypeVerificationInvalid verification_valid TypeVerificationValid verification_code_invalid TypeVerificationCodeInvalid verification_code_valid TypeVerificationCodeValid otp TypeOTP stub TypeTestStub login_code_valid TypeLoginCodeValid registration_code_valid TypeRegistrationCodeValid + // recovery_invalid TypeRecoveryInvalid recovery_valid TypeRecoveryValid recovery_code_invalid TypeRecoveryCodeInvalid recovery_code_valid TypeRecoveryCodeValid verification_invalid TypeVerificationInvalid verification_valid TypeVerificationValid verification_code_invalid TypeVerificationCodeInvalid verification_code_valid TypeVerificationCodeValid stub TypeTestStub login_code_valid TypeLoginCodeValid registration_code_valid TypeRegistrationCodeValid TemplateType string `json:"template_type"` Type CourierMessageType `json:"type"` // UpdatedAt is a helper struct field for gobuffalo.pop. @@ -86,6 +87,38 @@ func (o *Message) SetBody(v string) { o.Body = v } +// GetChannel returns the Channel field value if set, zero value otherwise. +func (o *Message) GetChannel() string { + if o == nil || o.Channel == nil { + var ret string + return ret + } + return *o.Channel +} + +// GetChannelOk returns a tuple with the Channel field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Message) GetChannelOk() (*string, bool) { + if o == nil || o.Channel == nil { + return nil, false + } + return o.Channel, true +} + +// HasChannel returns a boolean if a field has been set. +func (o *Message) HasChannel() bool { + if o != nil && o.Channel != nil { + return true + } + + return false +} + +// SetChannel gets a reference to the given string and assigns it to the Channel field. +func (o *Message) SetChannel(v string) { + o.Channel = &v +} + // GetCreatedAt returns the CreatedAt field value func (o *Message) GetCreatedAt() time.Time { if o == nil { @@ -339,6 +372,9 @@ func (o Message) MarshalJSON() ([]byte, error) { if true { toSerialize["body"] = o.Body } + if o.Channel != nil { + toSerialize["channel"] = o.Channel + } if true { toSerialize["created_at"] = o.CreatedAt } diff --git a/internal/httpclient/model_verifiable_identity_address.go b/internal/httpclient/model_verifiable_identity_address.go index 516a8107e40a..820881b2d3a2 100644 --- a/internal/httpclient/model_verifiable_identity_address.go +++ b/internal/httpclient/model_verifiable_identity_address.go @@ -31,7 +31,7 @@ type VerifiableIdentityAddress struct { // Indicates if the address has already been verified Verified bool `json:"verified"` VerifiedAt *time.Time `json:"verified_at,omitempty"` - // VerifiableAddressType must not exceed 16 characters as that is the limitation in the SQL Schema + // The delivery method Via string `json:"via"` } diff --git a/internal/testhelpers/session.go b/internal/testhelpers/session.go index 91a2da5abda2..e6e9da316ffd 100644 --- a/internal/testhelpers/session.go +++ b/internal/testhelpers/session.go @@ -251,3 +251,13 @@ func (ct *TransportWithHeader) RoundTrip(req *http.Request) (*http.Response, err } return ct.RoundTripper.RoundTrip(req) } + +func AssertNoCSRFCookieInResponse(t *testing.T, _ *httptest.Server, _ *http.Client, r *http.Response) { + found := false + for _, c := range r.Cookies() { + if strings.HasPrefix(c.Name, "csrf_token") { + found = true + } + } + require.False(t, found) +} diff --git a/openapitools.json b/openapitools.json index f5f966a1030d..64f2cbb54164 100644 --- a/openapitools.json +++ b/openapitools.json @@ -2,6 +2,6 @@ "$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json", "spaces": 2, "generator-cli": { - "version": "6.2.1" + "version": "7.2.0" } } diff --git a/package-lock.json b/package-lock.json index e2e32697e06a..8f860f034e72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,8 +4,9 @@ "requires": true, "packages": { "": { + "name": "kratos", "dependencies": { - "@openapitools/openapi-generator-cli": "2.6.0", + "@openapitools/openapi-generator-cli": "2.7.0", "yamljs": "0.3.0" }, "devDependencies": { @@ -41,35 +42,33 @@ } }, "node_modules/@nestjs/axios": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-0.0.8.tgz", - "integrity": "sha512-oJyfR9/h9tVk776il0829xyj3b2e81yTu6HjPraxynwNtMNGqZBHHmAQL24yMB3tVbBM0RvG3eUXH8+pRCGwlg==", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-0.1.0.tgz", + "integrity": "sha512-b2TT2X6BFbnNoeteiaxCIiHaFcSbVW+S5yygYqiIq5i6H77yIU3IVuLdpQkHq8/EqOWFwMopLN8jdkUT71Am9w==", "dependencies": { "axios": "0.27.2" }, "peerDependencies": { - "@nestjs/common": "^7.0.0 || ^8.0.0", + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0", "reflect-metadata": "^0.1.12", "rxjs": "^6.0.0 || ^7.0.0" } }, "node_modules/@nestjs/common": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-8.4.7.tgz", - "integrity": "sha512-m/YsbcBal+gA5CFrDpqXqsSfylo+DIQrkFY3qhVIltsYRfu8ct8J9pqsTO6OPf3mvqdOpFGrV5sBjoyAzOBvsw==", - "peer": true, + "version": "9.3.11", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-9.3.11.tgz", + "integrity": "sha512-IFZ2G/5UKWC2Uo7tJ4SxGed2+aiA+sJyWeWsGTogKVDhq90oxVBToh+uCDeI31HNUpqYGoWmkletfty42zUd8A==", "dependencies": { - "axios": "0.27.2", "iterare": "1.2.1", - "tslib": "2.4.0", - "uuid": "8.3.2" + "tslib": "2.5.0", + "uid": "2.0.1" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/nest" }, "peerDependencies": { - "cache-manager": "*", + "cache-manager": "<=5", "class-transformer": "*", "class-validator": "*", "reflect-metadata": "^0.1.12", @@ -88,10 +87,9 @@ } }, "node_modules/@nestjs/common/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "peer": true + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -146,12 +144,12 @@ } }, "node_modules/@openapitools/openapi-generator-cli": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.6.0.tgz", - "integrity": "sha512-M/aOpR7G+Y1nMf+ofuar8pGszajgfhs1aSPSijkcr2tHTxKAI3sA3YYcOGbszxaNRKFyvOcDq+KP9pcJvKoCHg==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.7.0.tgz", + "integrity": "sha512-ieEpHTA/KsDz7ANw03lLPYyjdedDEXYEyYoGBRWdduqXWSX65CJtttjqa8ZaB1mNmIjMtchUHwAYQmTLVQ8HYg==", "hasInstallScript": true, "dependencies": { - "@nestjs/axios": "0.0.8", + "@nestjs/axios": "0.1.0", "@nestjs/common": "9.3.11", "@nestjs/core": "9.3.11", "@nuxtjs/opencollective": "0.3.2", @@ -179,43 +177,6 @@ "url": "https://opencollective.com/openapi_generator" } }, - "node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/common": { - "version": "9.3.11", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-9.3.11.tgz", - "integrity": "sha512-IFZ2G/5UKWC2Uo7tJ4SxGed2+aiA+sJyWeWsGTogKVDhq90oxVBToh+uCDeI31HNUpqYGoWmkletfty42zUd8A==", - "dependencies": { - "iterare": "1.2.1", - "tslib": "2.5.0", - "uid": "2.0.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nest" - }, - "peerDependencies": { - "cache-manager": "<=5", - "class-transformer": "*", - "class-validator": "*", - "reflect-metadata": "^0.1.12", - "rxjs": "^7.1.0" - }, - "peerDependenciesMeta": { - "cache-manager": { - "optional": true - }, - "class-transformer": { - "optional": true - }, - "class-validator": { - "optional": true - } - } - }, - "node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/common/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" - }, "node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/core": { "version": "9.3.11", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-9.3.11.tgz", @@ -268,9 +229,9 @@ } }, "node_modules/@sideway/formula": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", - "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", "dev": true }, "node_modules/@sideway/pinpoint": { @@ -389,19 +350,6 @@ "form-data": "^4.0.0" } }, - "node_modules/axios/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -847,9 +795,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", - "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", "funding": [ { "type": "individual", @@ -865,6 +813,19 @@ } } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -1261,15 +1222,6 @@ "node": ">=4" } }, - "node_modules/license-checker/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, "node_modules/license-checker/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1435,15 +1387,6 @@ "validate-npm-package-license": "^3.0.1" } }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, "node_modules/npm-normalize-package-bin": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", @@ -1640,15 +1583,6 @@ "graceful-fs": "^4.1.2" } }, - "node_modules/read-installed/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, "node_modules/read-package-json": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz", @@ -1806,6 +1740,15 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -2062,15 +2005,6 @@ "integrity": "sha512-mLs5zAK+ctllYBj+iAQvlDCwoxU/WDOUaJkcFudeiAX6OajC6BKXJUa9a+tbtkC11dz2Ufb7h0lyvIOVn4LADA==", "dev": true }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "peer": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -2239,30 +2173,27 @@ "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==" }, "@nestjs/axios": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-0.0.8.tgz", - "integrity": "sha512-oJyfR9/h9tVk776il0829xyj3b2e81yTu6HjPraxynwNtMNGqZBHHmAQL24yMB3tVbBM0RvG3eUXH8+pRCGwlg==", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-0.1.0.tgz", + "integrity": "sha512-b2TT2X6BFbnNoeteiaxCIiHaFcSbVW+S5yygYqiIq5i6H77yIU3IVuLdpQkHq8/EqOWFwMopLN8jdkUT71Am9w==", "requires": { "axios": "0.27.2" } }, "@nestjs/common": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-8.4.7.tgz", - "integrity": "sha512-m/YsbcBal+gA5CFrDpqXqsSfylo+DIQrkFY3qhVIltsYRfu8ct8J9pqsTO6OPf3mvqdOpFGrV5sBjoyAzOBvsw==", - "peer": true, + "version": "9.3.11", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-9.3.11.tgz", + "integrity": "sha512-IFZ2G/5UKWC2Uo7tJ4SxGed2+aiA+sJyWeWsGTogKVDhq90oxVBToh+uCDeI31HNUpqYGoWmkletfty42zUd8A==", "requires": { - "axios": "0.27.2", "iterare": "1.2.1", - "tslib": "2.4.0", - "uuid": "8.3.2" + "tslib": "2.5.0", + "uid": "2.0.1" }, "dependencies": { "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "peer": true + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" } } }, @@ -2303,11 +2234,11 @@ } }, "@openapitools/openapi-generator-cli": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.6.0.tgz", - "integrity": "sha512-M/aOpR7G+Y1nMf+ofuar8pGszajgfhs1aSPSijkcr2tHTxKAI3sA3YYcOGbszxaNRKFyvOcDq+KP9pcJvKoCHg==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.7.0.tgz", + "integrity": "sha512-ieEpHTA/KsDz7ANw03lLPYyjdedDEXYEyYoGBRWdduqXWSX65CJtttjqa8ZaB1mNmIjMtchUHwAYQmTLVQ8HYg==", "requires": { - "@nestjs/axios": "0.0.8", + "@nestjs/axios": "0.1.0", "@nestjs/common": "9.3.11", "@nestjs/core": "9.3.11", "@nuxtjs/opencollective": "0.3.2", @@ -2325,23 +2256,6 @@ "tslib": "2.0.3" }, "dependencies": { - "@nestjs/common": { - "version": "9.3.11", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-9.3.11.tgz", - "integrity": "sha512-IFZ2G/5UKWC2Uo7tJ4SxGed2+aiA+sJyWeWsGTogKVDhq90oxVBToh+uCDeI31HNUpqYGoWmkletfty42zUd8A==", - "requires": { - "iterare": "1.2.1", - "tslib": "2.5.0", - "uid": "2.0.1" - }, - "dependencies": { - "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" - } - } - }, "@nestjs/core": { "version": "9.3.11", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-9.3.11.tgz", @@ -2374,9 +2288,9 @@ } }, "@sideway/formula": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", - "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", "dev": true }, "@sideway/pinpoint": { @@ -2472,18 +2386,6 @@ "requires": { "follow-redirects": "^1.14.9", "form-data": "^4.0.0" - }, - "dependencies": { - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - } } }, "balanced-match": { @@ -2810,9 +2712,19 @@ } }, "follow-redirects": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", - "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } }, "fs-extra": { "version": "10.1.0", @@ -3116,12 +3028,6 @@ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -3243,14 +3149,6 @@ "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } } }, "npm-normalize-package-bin": { @@ -3386,14 +3284,6 @@ "semver": "2 || 3 || 4 || 5", "slide": "~1.1.3", "util-extend": "^1.0.1" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } } }, "read-package-json": { @@ -3505,6 +3395,12 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + }, "signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -3716,12 +3612,6 @@ "integrity": "sha512-mLs5zAK+ctllYBj+iAQvlDCwoxU/WDOUaJkcFudeiAX6OajC6BKXJUa9a+tbtkC11dz2Ufb7h0lyvIOVn4LADA==", "dev": true }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "peer": true - }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", diff --git a/package.json b/package.json index 6c2735e4db2d..944faa190bf8 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ }, "prettier": "ory-prettier-styles", "dependencies": { - "@openapitools/openapi-generator-cli": "2.6.0", + "@openapitools/openapi-generator-cli": "2.7.0", "yamljs": "0.3.0" }, "devDependencies": { diff --git a/persistence/sql/identity/persister_identity.go b/persistence/sql/identity/persister_identity.go index 82d7b035e295..1bfbf107ee0a 100644 --- a/persistence/sql/identity/persister_identity.go +++ b/persistence/sql/identity/persister_identity.go @@ -957,7 +957,7 @@ func (p *IdentityPersister) GetIdentityConfidential(ctx context.Context, id uuid return p.GetIdentity(ctx, id, identity.ExpandEverything) } -func (p *IdentityPersister) FindVerifiableAddressByValue(ctx context.Context, via identity.VerifiableAddressType, value string) (_ *identity.VerifiableAddress, err error) { +func (p *IdentityPersister) FindVerifiableAddressByValue(ctx context.Context, via string, value string) (_ *identity.VerifiableAddress, err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.FindVerifiableAddressByValue") otelx.End(span, &err) diff --git a/persistence/sql/migrations/sql/20231130094628000000_courier_message_channel.down.sql b/persistence/sql/migrations/sql/20231130094628000000_courier_message_channel.down.sql new file mode 100644 index 000000000000..a43df5063cec --- /dev/null +++ b/persistence/sql/migrations/sql/20231130094628000000_courier_message_channel.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE + courier_messages DROP column channel; diff --git a/persistence/sql/migrations/sql/20231130094628000000_courier_message_channel.up.sql b/persistence/sql/migrations/sql/20231130094628000000_courier_message_channel.up.sql new file mode 100644 index 000000000000..a4075c0eecba --- /dev/null +++ b/persistence/sql/migrations/sql/20231130094628000000_courier_message_channel.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE + courier_messages +ADD + column channel VARCHAR(32) NULL; \ No newline at end of file diff --git a/persistence/sql/persister.go b/persistence/sql/persister.go index 82df92a72314..fa553cd559f5 100644 --- a/persistence/sql/persister.go +++ b/persistence/sql/persister.go @@ -13,7 +13,7 @@ import ( "github.com/gofrs/uuid" "github.com/laher/mergefs" "github.com/pkg/errors" - "github.com/sirupsen/logrus/hooks/test" + "github.com/sirupsen/logrus" "github.com/ory/kratos/driver/config" "github.com/ory/kratos/identity" @@ -24,7 +24,6 @@ import ( "github.com/ory/kratos/session" "github.com/ory/kratos/x" "github.com/ory/x/contextx" - "github.com/ory/x/logrusx" "github.com/ory/x/networkx" "github.com/ory/x/popx" ) @@ -82,8 +81,7 @@ func NewPersister(ctx context.Context, r persisterDependencies, c *pop.Connectio } logger := r.Logger() if o.disableLogging { - inner, _ := test.NewNullLogger() - logger = logrusx.New("kratos", "", logrusx.UseLogger(inner)) + logger.Logrus().SetLevel(logrus.WarnLevel) } m, err := popx.NewMigrationBox( mergefs.Merge( diff --git a/request/builder.go b/request/builder.go index 48d43856edf1..f930bc089aae 100644 --- a/request/builder.go +++ b/request/builder.go @@ -163,23 +163,23 @@ func (b *Builder) addURLEncodedBody(ctx context.Context, template *bytes.Buffer, enc.SetIndent("", "") if err := enc.Encode(body); err != nil { - return err + return errors.WithStack(err) } vm, err := b.deps.JsonnetVM(ctx) if err != nil { - return err + return errors.WithStack(err) } vm.TLACode("ctx", buf.String()) res, err := vm.EvaluateAnonymousSnippet(b.Config.TemplateURI, template.String()) if err != nil { - return err + return errors.WithStack(err) } values := map[string]string{} if err := json.Unmarshal([]byte(res), &values); err != nil { - return err + return errors.WithStack(err) } u := url.Values{} diff --git a/schema/extension.go b/schema/extension.go index 5b605cfb91d4..c217f409c8c3 100644 --- a/schema/extension.go +++ b/schema/extension.go @@ -41,30 +41,42 @@ type ( Recovery struct { Via string `json:"via"` } `json:"recovery"` - Mappings struct { - Identity struct { - Traits []struct { - Path string `json:"path"` - } `json:"traits"` - } `json:"identity"` - } `json:"mappings"` + RawSchema map[string]interface{} `json:"-"` } - Extension interface { + ValidateExtension interface { Run(ctx jsonschema.ValidationContext, config ExtensionConfig, value interface{}) error Finish() error } + CompileExtension interface { + Run(ctx jsonschema.CompilerContext, config ExtensionConfig, rawSchema map[string]interface{}) error + } ExtensionRunner struct { meta *jsonschema.Schema - compile func(ctx jsonschema.CompilerContext, m map[string]interface{}) (interface{}, error) + compile func(ctx jsonschema.CompilerContext, rawSchema map[string]interface{}) (interface{}, error) validate func(ctx jsonschema.ValidationContext, s interface{}, v interface{}) error - runners []Extension + validateRunners []ValidateExtension + compileRunners []CompileExtension } + + ExtensionRunnerOption func(*ExtensionRunner) ) -func NewExtensionRunner(ctx context.Context, runners ...Extension) (*ExtensionRunner, error) { +func WithValidateRunners(runners ...ValidateExtension) ExtensionRunnerOption { + return func(r *ExtensionRunner) { + r.validateRunners = append(r.validateRunners, runners...) + } +} + +func WithCompileRunners(runners ...CompileExtension) ExtensionRunnerOption { + return func(r *ExtensionRunner) { + r.compileRunners = append(r.compileRunners, runners...) + } +} + +func NewExtensionRunner(ctx context.Context, opts ...ExtensionRunnerOption) (*ExtensionRunner, error) { var err error r := new(ExtensionRunner) c := jsonschema.NewCompiler() @@ -90,6 +102,13 @@ func NewExtensionRunner(ctx context.Context, runners ...Extension) (*ExtensionRu return nil, errors.WithStack(err) } + for _, runner := range r.compileRunners { + if err := runner.Run(ctx, e, m); err != nil { + return nil, err + } + } + e.RawSchema = m + return &e, nil } return nil, nil @@ -101,7 +120,7 @@ func NewExtensionRunner(ctx context.Context, runners ...Extension) (*ExtensionRu return nil } - for _, runner := range r.runners { + for _, runner := range r.validateRunners { if err := runner.Run(ctx, *c, v); err != nil { return err } @@ -109,7 +128,10 @@ func NewExtensionRunner(ctx context.Context, runners ...Extension) (*ExtensionRu return nil } - r.runners = runners + for _, opt := range opts { + opt(r) + } + return r, nil } @@ -126,13 +148,13 @@ func (r *ExtensionRunner) Extension() jsonschema.Extension { } } -func (r *ExtensionRunner) AddRunner(run Extension) *ExtensionRunner { - r.runners = append(r.runners, run) +func (r *ExtensionRunner) AddRunner(run ValidateExtension) *ExtensionRunner { + r.validateRunners = append(r.validateRunners, run) return r } func (r *ExtensionRunner) Finish() error { - for _, runner := range r.runners { + for _, runner := range r.validateRunners { if err := runner.Finish(); err != nil { return err } diff --git a/selfservice/flow/login/error_test.go b/selfservice/flow/login/error_test.go index c6b44b75cf3e..5cc78c35bda1 100644 --- a/selfservice/flow/login/error_test.go +++ b/selfservice/flow/login/error_test.go @@ -43,6 +43,8 @@ func TestHandleError(t *testing.T) { conf, reg := internal.NewFastRegistryWithMocks(t) public, _ := testhelpers.NewKratosServer(t, reg) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/password.schema.json") + router := httprouter.New() ts := httptest.NewServer(router) t.Cleanup(ts.Close) diff --git a/selfservice/flow/login/extension_identifier_label.go b/selfservice/flow/login/extension_identifier_label.go new file mode 100644 index 000000000000..02b725f00b7f --- /dev/null +++ b/selfservice/flow/login/extension_identifier_label.go @@ -0,0 +1,66 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + "context" + + "github.com/ory/kratos/text" + + "github.com/ory/jsonschema/v3" + "github.com/ory/kratos/schema" +) + +type identifierLabelExtension struct { + identifierLabelCandidates []string +} + +var _ schema.CompileExtension = new(identifierLabelExtension) + +func GetIdentifierLabelFromSchema(ctx context.Context, schemaURL string) (*text.Message, error) { + ext := &identifierLabelExtension{} + + runner, err := schema.NewExtensionRunner(ctx, schema.WithCompileRunners(ext)) + if err != nil { + return nil, err + } + c := jsonschema.NewCompiler() + runner.Register(c) + + _, err = c.Compile(ctx, schemaURL) + if err != nil { + return nil, err + } + metaLabel := text.NewInfoNodeLabelID() + if label := ext.getLabel(); label != "" { + metaLabel = text.NewInfoNodeLabelGenerated(label) + } + return metaLabel, nil +} + +func (i *identifierLabelExtension) Run(_ jsonschema.CompilerContext, config schema.ExtensionConfig, rawSchema map[string]interface{}) error { + if config.Credentials.Password.Identifier || + config.Credentials.WebAuthn.Identifier || + config.Credentials.TOTP.AccountName || + config.Credentials.Code.Identifier { + if title, ok := rawSchema["title"]; ok { + // The jsonschema compiler validates the title to be a string, so this should always work. + switch t := title.(type) { + case string: + if t != "" { + i.identifierLabelCandidates = append(i.identifierLabelCandidates, t) + } + } + } + } + return nil +} + +func (i *identifierLabelExtension) getLabel() string { + if len(i.identifierLabelCandidates) != 1 { + // sane default is set elsewhere + return "" + } + return i.identifierLabelCandidates[0] +} diff --git a/selfservice/flow/login/extension_identifier_label_test.go b/selfservice/flow/login/extension_identifier_label_test.go new file mode 100644 index 000000000000..3b3a21400fb1 --- /dev/null +++ b/selfservice/flow/login/extension_identifier_label_test.go @@ -0,0 +1,143 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "testing" + + "github.com/ory/kratos/text" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/sjson" + + "github.com/ory/kratos/schema" +) + +func constructSchema(t *testing.T, ecModifier, ucModifier func(*schema.ExtensionConfig)) string { + var emailConfig, usernameConfig schema.ExtensionConfig + + if ecModifier != nil { + ecModifier(&emailConfig) + } + if ucModifier != nil { + ucModifier(&usernameConfig) + } + + ec, err := json.Marshal(&emailConfig) + require.NoError(t, err) + uc, err := json.Marshal(&usernameConfig) + require.NoError(t, err) + + ec, err = sjson.DeleteBytes(ec, "verification") + require.NoError(t, err) + ec, err = sjson.DeleteBytes(ec, "recovery") + require.NoError(t, err) + ec, err = sjson.DeleteBytes(ec, "credentials.code.via") + require.NoError(t, err) + uc, err = sjson.DeleteBytes(uc, "verification") + require.NoError(t, err) + uc, err = sjson.DeleteBytes(uc, "recovery") + require.NoError(t, err) + uc, err = sjson.DeleteBytes(uc, "credentials.code.via") + require.NoError(t, err) + + return "base64://" + base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(` +{ + "properties": { + "traits": { + "properties": { + "email": { + "title": "Email", + "ory.sh/kratos": %s + }, + "username": { + "title": "Username", + "ory.sh/kratos": %s + } + } + } + } +}`, ec, uc))) +} + +func TestGetIdentifierLabelFromSchema(t *testing.T) { + ctx := context.Background() + + for _, tc := range []struct { + name string + emailConfig, usernameConfig func(*schema.ExtensionConfig) + expected *text.Message + }{ + { + name: "email for password", + emailConfig: func(c *schema.ExtensionConfig) { + c.Credentials.Password.Identifier = true + }, + expected: text.NewInfoNodeLabelGenerated("Email"), + }, + { + name: "email for webauthn", + emailConfig: func(c *schema.ExtensionConfig) { + c.Credentials.WebAuthn.Identifier = true + }, + expected: text.NewInfoNodeLabelGenerated("Email"), + }, + { + name: "email for totp", + emailConfig: func(c *schema.ExtensionConfig) { + c.Credentials.TOTP.AccountName = true + }, + expected: text.NewInfoNodeLabelGenerated("Email"), + }, + { + name: "email for code", + emailConfig: func(c *schema.ExtensionConfig) { + c.Credentials.Code.Identifier = true + }, + expected: text.NewInfoNodeLabelGenerated("Email"), + }, + { + name: "email for all", + emailConfig: func(c *schema.ExtensionConfig) { + c.Credentials.Password.Identifier = true + c.Credentials.WebAuthn.Identifier = true + c.Credentials.TOTP.AccountName = true + c.Credentials.Code.Identifier = true + }, + expected: text.NewInfoNodeLabelGenerated("Email"), + }, + { + name: "username works as well", + usernameConfig: func(c *schema.ExtensionConfig) { + c.Credentials.Password.Identifier = true + }, + expected: text.NewInfoNodeLabelGenerated("Username"), + }, + { + name: "multiple identifiers", + emailConfig: func(c *schema.ExtensionConfig) { + c.Credentials.Password.Identifier = true + }, + usernameConfig: func(c *schema.ExtensionConfig) { + c.Credentials.Password.Identifier = true + }, + expected: text.NewInfoNodeLabelID(), + }, + { + name: "no identifiers", + expected: text.NewInfoNodeLabelID(), + }, + } { + t.Run(tc.name, func(t *testing.T) { + label, err := GetIdentifierLabelFromSchema(ctx, constructSchema(t, tc.emailConfig, tc.usernameConfig)) + require.NoError(t, err) + assert.Equal(t, tc.expected, label) + }) + } +} diff --git a/selfservice/flow/login/handler_test.go b/selfservice/flow/login/handler_test.go index 85daee4e65a9..9a82e85ccbc7 100644 --- a/selfservice/flow/login/handler_test.go +++ b/selfservice/flow/login/handler_test.go @@ -797,6 +797,8 @@ func TestGetFlow(t *testing.T) { _ = testhelpers.NewErrorTestServer(t, reg) _ = testhelpers.NewRedirTS(t, "", conf) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/password.schema.json") + setupLoginUI := func(t *testing.T, c *http.Client) *httptest.Server { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // It is important that we use a HTTP request to fetch the flow because that will show us if CSRF works or not diff --git a/selfservice/flow/login/hook.go b/selfservice/flow/login/hook.go index 2af0c3daf1fc..620559f57b06 100644 --- a/selfservice/flow/login/hook.go +++ b/selfservice/flow/login/hook.go @@ -257,6 +257,16 @@ func (e *HookExecutor) PostLoginHook( // Browser flows rely on cookies. Adding tokens in the mix will confuse consumers. s.Token = "" + // If we detect that whoami would require a higher AAL, we redirect! + if _, err := e.requiresAAL2(r, s, a); err != nil { + if aalErr := new(session.ErrAALNotSatisfied); errors.As(err, &aalErr) { + span.SetAttributes(attribute.String("return_to", aalErr.RedirectTo), attribute.String("redirect_reason", "requires aal2")) + e.d.Writer().WriteError(w, r, flow.NewBrowserLocationChangeRequiredError(aalErr.RedirectTo)) + return nil + } + return err + } + // If Kratos is used as a Hydra login provider, we need to redirect back to Hydra by returning a 422 status // with the post login challenge URL as the body. if a.OAuth2LoginChallenge != "" { @@ -275,16 +285,6 @@ func (e *HookExecutor) PostLoginHook( return nil } - // If we detect that whoami would require a higher AAL, we redirect! - if _, err := e.requiresAAL2(r, s, a); err != nil { - if aalErr := new(session.ErrAALNotSatisfied); errors.As(err, &aalErr) { - span.SetAttributes(attribute.String("return_to", aalErr.RedirectTo), attribute.String("redirect_reason", "requires aal2")) - e.d.Writer().WriteError(w, r, flow.NewBrowserLocationChangeRequiredError(aalErr.RedirectTo)) - return nil - } - return err - } - response := &APIFlowResponse{Session: s} e.d.Writer().Write(w, r, response) return nil diff --git a/selfservice/flow/login/hook_test.go b/selfservice/flow/login/hook_test.go index 052973317ca2..46f957c1ca76 100644 --- a/selfservice/flow/login/hook_test.go +++ b/selfservice/flow/login/hook_test.go @@ -231,6 +231,8 @@ func TestLoginExecutor(t *testing.T) { }) t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + conf.MustSet(ctx, config.ViperKeyOAuth2ProviderURL, "https://hydra") + useIdentity := &identity.Identity{Credentials: map[identity.CredentialsType]identity.Credentials{ identity.CredentialsTypePassword: {Type: identity.CredentialsTypePassword, Config: []byte(`{"hashed_password": "$argon2id$v=19$m=32,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw"}`), Identifiers: []string{testhelpers.RandomEmail()}}, identity.CredentialsTypeWebAuthn: {Type: identity.CredentialsTypeWebAuthn, Config: []byte(`{"credentials":[{"is_passwordless":false}]}`), Identifiers: []string{testhelpers.RandomEmail()}}, @@ -243,6 +245,17 @@ func TestLoginExecutor(t *testing.T) { assert.Contains(t, res.Request.URL.String(), "/self-service/login/browser?aal=aal2") }) + t.Run("browser client with login challenge", func(t *testing.T) { + res, _ := makeRequestPost(t, newServer(t, flow.TypeBrowser, useIdentity), false, url.Values{ + "login_challenge": []string{hydra.FakeValidLoginChallenge}, + }) + assert.EqualValues(t, http.StatusNotFound, res.StatusCode) + + assert.Equal(t, res.Request.URL.Path, "/self-service/login/browser") + assert.Equal(t, res.Request.URL.Query().Get("aal"), "aal2") + assert.Equal(t, res.Request.URL.Query().Get("login_challenge"), hydra.FakeValidLoginChallenge) + }) + t.Run("api client returns the token and the session without the identity", func(t *testing.T) { res, body := makeRequestPost(t, newServer(t, flow.TypeAPI, useIdentity), true, url.Values{}) assert.EqualValues(t, http.StatusOK, res.StatusCode) @@ -256,8 +269,22 @@ func TestLoginExecutor(t *testing.T) { assert.NotEmpty(t, gjson.Get(body, "redirect_browser_to").String()) assert.Contains(t, gjson.Get(body, "redirect_browser_to").String(), "/self-service/login/browser?aal=aal2", "%s", body) }) - }) + t.Run("browser JSON client with login challenge", func(t *testing.T) { + res, body := makeRequestPost(t, newServer(t, flow.TypeBrowser, useIdentity), true, url.Values{ + "login_challenge": []string{hydra.FakeValidLoginChallenge}, + }) + assert.EqualValues(t, http.StatusUnprocessableEntity, res.StatusCode) + assert.NotEmpty(t, gjson.Get(body, "redirect_browser_to").String()) + + redirectBrowserTo, err := url.Parse(gjson.Get(body, "redirect_browser_to").String()) + require.NoError(t, err) + + assert.Equal(t, redirectBrowserTo.Path, "/self-service/login/browser") + assert.Equal(t, redirectBrowserTo.Query().Get("aal"), "aal2") + assert.Equal(t, redirectBrowserTo.Query().Get("login_challenge"), hydra.FakeValidLoginChallenge) + }) + }) }) t.Run("case=maybe links credential", func(t *testing.T) { t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) diff --git a/selfservice/flow/recovery/handler.go b/selfservice/flow/recovery/handler.go index 2a4c7f77dd01..6927a733dd8e 100644 --- a/selfservice/flow/recovery/handler.go +++ b/selfservice/flow/recovery/handler.go @@ -443,6 +443,11 @@ func (h *Handler) updateRecoveryFlow(w http.ResponseWriter, r *http.Request, ps return } + // WARNING - just because no error was returned does not mean that the challenge was accepted. Instead, the + // success state is available as: + // + // if f.State == flow.StatePassedChallenge + if f.Type == flow.TypeBrowser && !x.IsJSONRequest(r) { http.Redirect(w, r, f.AppendTo(h.d.Config().SelfServiceFlowRecoveryUI(r.Context())).String(), http.StatusSeeOther) return diff --git a/selfservice/flow/verification/handler.go b/selfservice/flow/verification/handler.go index 6e0852006f22..8b0e832ad8e7 100644 --- a/selfservice/flow/verification/handler.go +++ b/selfservice/flow/verification/handler.go @@ -444,7 +444,7 @@ func (h *Handler) updateVerificationFlow(w http.ResponseWriter, r *http.Request, if x.IsBrowserRequest(r) { // Special case: If we ended up here through a OAuth2 login challenge, we need to accept the login request // and redirect back to the OAuth2 provider. - if f.OAuth2LoginChallenge.String() != "" { + if flow.HasReachedState(flow.StatePassedChallenge, f.State) && f.OAuth2LoginChallenge.String() != "" { if !f.IdentityID.Valid || !f.SessionID.Valid { h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, herodot.ErrBadRequest.WithReasonf("No session was found for this flow. Please retry the authentication.")) @@ -468,6 +468,7 @@ func (h *Handler) updateVerificationFlow(w http.ResponseWriter, r *http.Request, h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, err) return } + err = h.d.SessionManager().IssueCookie(ctx, w, r, sess) if err != nil { h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, err) diff --git a/selfservice/strategy/code/code_sender.go b/selfservice/strategy/code/code_sender.go index 3317fafdb2e4..d41beef463ed 100644 --- a/selfservice/strategy/code/code_sender.go +++ b/selfservice/strategy/code/code_sender.go @@ -13,6 +13,7 @@ import ( "github.com/ory/herodot" "github.com/ory/kratos/courier/template/email" + "github.com/ory/kratos/courier/template/sms" "github.com/ory/x/httpx" "github.com/ory/x/sqlcon" @@ -240,7 +241,7 @@ func (s *Sender) SendRecoveryCodeTo(ctx context.Context, i *identity.Identity, c // If the address does not exist in the store and dispatching invalid emails is enabled (CourierEnableInvalidDispatch is // true), an email is still being sent to prevent account enumeration attacks. In that case, this function returns the // ErrUnknownAddress error. -func (s *Sender) SendVerificationCode(ctx context.Context, f *verification.Flow, via identity.VerifiableAddressType, to string) error { +func (s *Sender) SendVerificationCode(ctx context.Context, f *verification.Flow, via string, to string) error { s.deps.Logger(). WithField("via", via). WithSensitiveField("address", to). @@ -312,29 +313,62 @@ func (s *Sender) SendVerificationCodeTo(ctx context.Context, f *verification.Flo return err } - if err := s.send(ctx, string(code.VerifiableAddress.Via), email.NewVerificationCodeValid(s.deps, - &email.VerificationCodeValidModel{ + var t courier.Template + + // TODO: this can likely be abstracted by making templates not specific to the channel they're using + switch code.VerifiableAddress.Via { + case identity.ChannelTypeEmail: + t = email.NewVerificationCodeValid(s.deps, &email.VerificationCodeValidModel{ To: code.VerifiableAddress.Value, VerificationURL: s.constructVerificationLink(ctx, f.ID, codeString), Identity: model, VerificationCode: codeString, - })); err != nil { + }) + case identity.ChannelTypeSMS: + t = sms.NewVerificationCodeValid(s.deps, &sms.VerificationCodeValidModel{ + To: code.VerifiableAddress.Value, + VerificationCode: codeString, + Identity: model, + }) + default: + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Expected email or sms but got %s", code.VerifiableAddress.Via)) + } + + if err := s.send(ctx, string(code.VerifiableAddress.Via), t); err != nil { return err } code.VerifiableAddress.Status = identity.VerifiableAddressStatusSent return s.deps.PrivilegedIdentityPool().UpdateVerifiableAddress(ctx, code.VerifiableAddress) } -func (s *Sender) send(ctx context.Context, via string, t courier.EmailTemplate) error { +func (s *Sender) send(ctx context.Context, via string, t courier.Template) error { switch f := stringsx.SwitchExact(via); { - case f.AddCase(identity.AddressTypeEmail): + case f.AddCase(identity.ChannelTypeEmail): c, err := s.deps.Courier(ctx) if err != nil { return err } + t, ok := t.(courier.EmailTemplate) + if !ok { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Expected email template but got %T", t)) + } + _, err = c.QueueEmail(ctx, t) return err + case f.AddCase(identity.ChannelTypeSMS): + c, err := s.deps.Courier(ctx) + if err != nil { + return err + } + + t, ok := t.(courier.SMSTemplate) + if !ok { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Expected sms template but got %T", t)) + } + + _, err = c.QueueSMS(ctx, t) + return err default: return f.ToUnknownCaseErr() } diff --git a/selfservice/strategy/code/strategy.go b/selfservice/strategy/code/strategy.go index 8b21a89a69e2..a5d0c83703ec 100644 --- a/selfservice/strategy/code/strategy.go +++ b/selfservice/strategy/code/strategy.go @@ -149,9 +149,17 @@ func (s *Strategy) PopulateMethod(r *http.Request, f flow.Flow) error { WithMetaLabel(text.NewInfoNodeInputEmail()), ) } else if f.GetFlowName() == flow.LoginFlow { - // we use the identifier label here since we don't know what - // type of field the identifier is - nodes.Upsert(node.NewInputField("identifier", "", node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeLabelID())) + ds, err := s.deps.Config().DefaultIdentityTraitsSchemaURL(r.Context()) + if err != nil { + return err + } + + identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String()) + if err != nil { + return err + } + + nodes.Upsert(node.NewInputField("identifier", "", node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(identifierLabel)) } else if f.GetFlowName() == flow.RegistrationFlow { ds, err := s.deps.Config().DefaultIdentityTraitsSchemaURL(r.Context()) if err != nil { diff --git a/selfservice/strategy/code/strategy_login.go b/selfservice/strategy/code/strategy_login.go index 64a3ab13a325..1a0170d36648 100644 --- a/selfservice/strategy/code/strategy_login.go +++ b/selfservice/strategy/code/strategy_login.go @@ -22,7 +22,6 @@ import ( "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/session" - "github.com/ory/kratos/text" "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" "github.com/ory/x/decoderx" @@ -79,10 +78,18 @@ func (s *Strategy) HandleLoginError(r *http.Request, f *login.Flow, body *update email = body.Identifier } + ds, err := s.deps.Config().DefaultIdentityTraitsSchemaURL(r.Context()) + if err != nil { + return err + } + identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String()) + if err != nil { + return err + } f.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) f.UI.GetNodes().Upsert( node.NewInputField("identifier", email, node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). - WithMetaLabel(text.NewInfoNodeLabelID()), + WithMetaLabel(identifierLabel), ) } diff --git a/selfservice/strategy/code/strategy_recovery.go b/selfservice/strategy/code/strategy_recovery.go index 8c5f2c86d980..2a917f0dcecd 100644 --- a/selfservice/strategy/code/strategy_recovery.go +++ b/selfservice/strategy/code/strategy_recovery.go @@ -254,8 +254,12 @@ func (s *Strategy) recoveryUseCode(w http.ResponseWriter, r *http.Request, body return s.retryRecoveryFlow(w, r, f.Type, RetryWithError(err)) } - // No error - return nil + if f.Type == flow.TypeBrowser && !x.IsJSONRequest(r) { + http.Redirect(w, r, f.AppendTo(s.deps.Config().SelfServiceFlowRecoveryUI(r.Context())).String(), http.StatusSeeOther) + } else { + s.deps.Writer().Write(w, r, f) + } + return errors.WithStack(flow.ErrCompletedByStrategy) } else if err != nil { return s.retryRecoveryFlow(w, r, f.Type, RetryWithError(err)) } diff --git a/selfservice/strategy/code/strategy_recovery_test.go b/selfservice/strategy/code/strategy_recovery_test.go index 91afe5c3e149..b481a37c8726 100644 --- a/selfservice/strategy/code/strategy_recovery_test.go +++ b/selfservice/strategy/code/strategy_recovery_test.go @@ -1597,7 +1597,7 @@ func TestRecovery_WithContinueWith(t *testing.T) { t.Run("type="+testCase.ClientType.String(), func(t *testing.T) { recoveryEmail := testhelpers.RandomEmail() createIdentityToRecover(t, reg, recoveryEmail) - conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryRequestLifespan, time.Millisecond*10) + conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryRequestLifespan, time.Millisecond*100) t.Cleanup(func() { conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryRequestLifespan, time.Minute) }) @@ -1611,7 +1611,7 @@ func TestRecovery_WithContinueWith(t *testing.T) { fallthrough case RecoveryClientTypeSPA: rs = testhelpers.GetRecoveryFlow(t, c, public) - time.Sleep(time.Millisecond * 11) + time.Sleep(time.Millisecond * 110) res, err = c.PostForm(rs.Ui.Action, url.Values{"email": {recoveryEmail}, "method": {"code"}}) require.NoError(t, err) assert.EqualValues(t, http.StatusOK, res.StatusCode) @@ -1619,7 +1619,7 @@ func TestRecovery_WithContinueWith(t *testing.T) { assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowRecoveryUI(ctx).String()) case RecoveryClientTypeAPI: rs = testhelpers.InitializeRecoveryFlowViaAPI(t, c, public) - time.Sleep(time.Millisecond * 11) + time.Sleep(time.Millisecond * 110) form := testhelpers.EncodeFormAsJSON(t, true, url.Values{"email": {recoveryEmail}, "method": {"code"}}) res, err = c.Post(rs.Ui.Action, "application/json", bytes.NewBufferString(form)) require.NoError(t, err) diff --git a/selfservice/strategy/code/strategy_verification.go b/selfservice/strategy/code/strategy_verification.go index 28e21456323e..e6969fda738d 100644 --- a/selfservice/strategy/code/strategy_verification.go +++ b/selfservice/strategy/code/strategy_verification.go @@ -236,8 +236,12 @@ func (s *Strategy) verificationUseCode(w http.ResponseWriter, r *http.Request, c return s.retryVerificationFlowWithError(w, r, f.Type, err) } - // No error - return nil + if x.IsBrowserRequest(r) { + http.Redirect(w, r, f.AppendTo(s.deps.Config().SelfServiceFlowVerificationUI(r.Context())).String(), http.StatusSeeOther) + } else { + s.deps.Writer().Write(w, r, f) + } + return errors.WithStack(flow.ErrCompletedByStrategy) } else if err != nil { return s.retryVerificationFlowWithError(w, r, f.Type, err) } diff --git a/selfservice/strategy/code/strategy_verification_test.go b/selfservice/strategy/code/strategy_verification_test.go index cdbfedc6069d..0f6a9fe15a10 100644 --- a/selfservice/strategy/code/strategy_verification_test.go +++ b/selfservice/strategy/code/strategy_verification_test.go @@ -16,6 +16,8 @@ import ( "testing" "time" + "github.com/gofrs/uuid" + "github.com/ory/x/urlx" "github.com/ory/kratos/selfservice/strategy/code" @@ -377,6 +379,11 @@ func TestVerification(t *testing.T) { f, err := verification.NewFlow(conf, time.Hour, x.FakeCSRFToken, httptest.NewRequest("GET", requestURL, nil), code.NewStrategy(reg), fType) require.NoError(t, err) f.State = flow.StateEmailSent + u, err := url.Parse(f.RequestURL) + require.NoError(t, err) + f.OAuth2LoginChallenge = sqlxx.NullString(u.Query().Get("login_challenge")) + f.IdentityID = uuid.NullUUID{UUID: x.NewUUID(), Valid: true} + f.SessionID = uuid.NullUUID{UUID: x.NewUUID(), Valid: true} require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(context.Background(), f)) email := identity.NewVerifiableEmailAddress(verificationEmail, identityToVerify.ID) identityToVerify.VerifiableAddresses = append(identityToVerify.VerifiableAddresses, *email) @@ -634,4 +641,25 @@ func TestVerification(t *testing.T) { }) } }) + + t.Run("case=doesn't continue with OAuth2 flow if code is invalid", func(t *testing.T) { + returnToURL := public.URL + "/after-verification" + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnToURL}) + + client := testhelpers.NewClientWithCookies(t) + flow, _, _ := newValidFlow(t, flow.TypeBrowser, public.URL+verification.RouteInitBrowserFlow+"?"+url.Values{"return_to": {returnToURL}, "login_challenge": {"any_valid_challenge"}}.Encode()) + + body := fmt.Sprintf( + `{"csrf_token":"%s","code":"%s"}`, flow.CSRFToken, "2475", + ) + + res, err := client.Post(public.URL+verification.RouteSubmitFlow+"?"+url.Values{"flow": {flow.ID.String()}}.Encode(), "application/json", bytes.NewBuffer([]byte(body))) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + responseBody := gjson.ParseBytes(ioutilx.MustReadAll(res.Body)) + + assert.Equal(t, responseBody.Get("state").String(), "sent_email", "%v", responseBody) + assert.Len(t, responseBody.Get("ui.messages").Array(), 1, "%v", responseBody) + assert.Equal(t, "The verification code is invalid or has already been used. Please try again.", responseBody.Get("ui.messages.0.text").String(), "%v", responseBody) + }) } diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go index 861547874cb5..e67a5826b57e 100644 --- a/selfservice/strategy/oidc/strategy.go +++ b/selfservice/strategy/oidc/strategy.go @@ -210,9 +210,9 @@ func (s *Strategy) setRoutes(r *x.RouterPublic) { // Apple can use the POST request method when calling the callback if handle, _, _ := r.Lookup("POST", RouteCallback); handle == nil { // Hardcoded path to Apple provider, I don't have a better way of doing it right now. - // Also this exempt disables CSRF checks for both GET and POST requests. Unfortunately + // Also this ignore disables CSRF checks for both GET and POST requests. Unfortunately // CSRF handler does not allow to define a rule based on the request method, at least not yet. - s.d.CSRFHandler().ExemptPath(RouteBase + "/callback/apple") + s.d.CSRFHandler().IgnorePath(RouteBase + "/callback/apple") // When handler is called using POST method, the cookies are not attached to the request // by the browser. So here we just redirect the request to the same location rewriting the @@ -570,7 +570,7 @@ func (s *Strategy) forwardError(w http.ResponseWriter, r *http.Request, f flow.F } } -func (s *Strategy) handleError(w http.ResponseWriter, r *http.Request, f flow.Flow, provider string, traits []byte, err error) error { +func (s *Strategy) handleError(w http.ResponseWriter, r *http.Request, f flow.Flow, providerID string, traits []byte, err error) error { switch rf := f.(type) { case *login.Flow: return err @@ -590,7 +590,7 @@ func (s *Strategy) handleError(w http.ResponseWriter, r *http.Request, f flow.Fl rf.UI.Messages.Add(text.NewErrorValidationDuplicateCredentialsOnOIDCLink()) } - lf, err := s.registrationToLogin(w, r, rf, provider) + lf, err := s.registrationToLogin(w, r, rf, providerID) if err != nil { return err } @@ -613,7 +613,12 @@ func (s *Strategy) handleError(w http.ResponseWriter, r *http.Request, f flow.Fl } newLoginURL := s.d.Config().SelfServiceFlowLoginUI(r.Context()).String() - lf.UI.Messages.Add(text.NewInfoLoginLinkMessage(dc.DuplicateIdentifier, provider, newLoginURL)) + providerLabel := providerID + provider, _ := s.provider(r.Context(), r, providerID) + if provider != nil && provider.Config() != nil { + providerLabel = provider.Config().Label + } + lf.UI.Messages.Add(text.NewInfoLoginLinkMessage(dc.DuplicateIdentifier, providerLabel, newLoginURL)) err := s.d.LoginFlowPersister().UpdateLoginFlow(r.Context(), lf) if err != nil { @@ -629,7 +634,7 @@ func (s *Strategy) handleError(w http.ResponseWriter, r *http.Request, f flow.Fl // Adds the "Continue" button rf.UI.SetCSRF(s.d.GenerateCSRFToken(r)) - AddProvider(rf.UI, provider, text.NewInfoRegistrationContinue()) + AddProvider(rf.UI, providerID, text.NewInfoRegistrationContinue()) if traits != nil { ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) diff --git a/selfservice/strategy/oidc/strategy_test.go b/selfservice/strategy/oidc/strategy_test.go index c5d441bbd766..0b4ca95c6e9e 100644 --- a/selfservice/strategy/oidc/strategy_test.go +++ b/selfservice/strategy/oidc/strategy_test.go @@ -1417,14 +1417,13 @@ func TestPostEndpointRedirect(t *testing.T) { remoteAdmin, remotePublic, _ := newHydra(t, &subject, &claims, &scope) - publicTS, adminTS := testhelpers.NewKratosServers(t) + publicTS, _, _, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) viperSetProviderConfig( t, conf, newOIDCProvider(t, publicTS, remotePublic, remoteAdmin, "apple"), ) - testhelpers.InitKratosServers(t, reg, publicTS, adminTS) t.Run("case=should redirect to GET and preserve parameters"+publicTS.URL, func(t *testing.T) { // create a client that does not follow redirects @@ -1441,5 +1440,8 @@ func TestPostEndpointRedirect(t *testing.T) { location, err := res.Location() require.NoError(t, err) assert.Equal(t, publicTS.URL+"/self-service/methods/oidc/callback/apple?state=foo&test=3", location.String()) + + // We don't want to add/override CSRF cookie when redirecting + testhelpers.AssertNoCSRFCookieInResponse(t, publicTS, c, res) }) } diff --git a/selfservice/strategy/password/login.go b/selfservice/strategy/password/login.go index 7a56ebaf45d4..5c9ed653872d 100644 --- a/selfservice/strategy/password/login.go +++ b/selfservice/strategy/password/login.go @@ -147,7 +147,15 @@ func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.Au sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) sr.UI.SetNode(node.NewInputField("identifier", identifier, node.DefaultGroup, node.InputAttributeTypeHidden)) } else { - sr.UI.SetNode(node.NewInputField("identifier", "", node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeLabelID())) + ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) + if err != nil { + return err + } + identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String()) + if err != nil { + return err + } + sr.UI.SetNode(node.NewInputField("identifier", "", node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(identifierLabel)) } sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) diff --git a/selfservice/strategy/password/validator.go b/selfservice/strategy/password/validator.go index 0875dff6a868..be1aac3c3497 100644 --- a/selfservice/strategy/password/validator.go +++ b/selfservice/strategy/password/validator.go @@ -14,13 +14,14 @@ import ( "strings" "time" + "go.opentelemetry.io/otel/trace/noop" + "github.com/ory/kratos/text" "github.com/arbovm/levenshtein" "github.com/dgraph-io/ristretto" "github.com/hashicorp/go-retryablehttp" "github.com/pkg/errors" - "go.opentelemetry.io/otel/trace" "github.com/ory/herodot" "github.com/ory/kratos/driver/config" @@ -88,7 +89,7 @@ func NewDefaultPasswordValidatorStrategy(reg validatorDependencies) (*DefaultPas // Tracing still works correctly even though we pass a no-op tracer // here, because the otelhttp package will preferentially use the // tracer from the incoming request context over this one. - httpx.ResilientClientWithTracer(trace.NewNoopTracerProvider().Tracer("github.com/ory/kratos/selfservice/strategy/password"))), + httpx.ResilientClientWithTracer(noop.NewTracerProvider().Tracer("github.com/ory/kratos/selfservice/strategy/password"))), reg: reg, hashes: cache, minIdentifierPasswordDist: 5, maxIdentifierPasswordSubstrThreshold: 0.5}, nil diff --git a/selfservice/strategy/webauthn/login.go b/selfservice/strategy/webauthn/login.go index 7b968c704151..6b9ee3a154f9 100644 --- a/selfservice/strategy/webauthn/login.go +++ b/selfservice/strategy/webauthn/login.go @@ -90,8 +90,17 @@ func (s *Strategy) populateLoginMethodForPasswordless(r *http.Request, sr *login return nil } + ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) + if err != nil { + return err + } + identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String()) + if err != nil { + return err + } + sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) - sr.UI.SetNode(node.NewInputField("identifier", "", node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeLabelID())) + sr.UI.SetNode(node.NewInputField("identifier", "", node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(identifierLabel)) sr.UI.GetNodes().Append(node.NewInputField("method", "webauthn", node.WebAuthnGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceLoginWebAuthn())) return nil } diff --git a/session/handler_test.go b/session/handler_test.go index 286943796927..b08fb4d1ecb5 100644 --- a/session/handler_test.go +++ b/session/handler_test.go @@ -51,16 +51,6 @@ func send(code int) httprouter.Handle { } } -func assertNoCSRFCookieInResponse(t *testing.T, _ *httptest.Server, _ *http.Client, r *http.Response) { - found := false - for _, c := range r.Cookies() { - if strings.HasPrefix(c.Name, "csrf_token") { - found = true - } - } - require.False(t, found) -} - func TestSessionWhoAmI(t *testing.T) { conf, reg := internal.NewFastRegistryWithMocks(t) ts, _, r, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) @@ -156,7 +146,7 @@ func TestSessionWhoAmI(t *testing.T) { // No cookie yet -> 401 res, err := client.Get(ts.URL + RouteWhoami) require.NoError(t, err) - assertNoCSRFCookieInResponse(t, ts, client, res) // Test that no CSRF cookie is ever set here. + testhelpers.AssertNoCSRFCookieInResponse(t, ts, client, res) // Test that no CSRF cookie is ever set here. if cacheEnabled { assert.NotEmpty(t, res.Header.Get("Ory-Session-Cache-For")) @@ -183,7 +173,7 @@ func TestSessionWhoAmI(t *testing.T) { require.NoError(t, err) body, err := io.ReadAll(res.Body) require.NoError(t, err) - assertNoCSRFCookieInResponse(t, ts, client, res) // Test that no CSRF cookie is ever set here. + testhelpers.AssertNoCSRFCookieInResponse(t, ts, client, res) // Test that no CSRF cookie is ever set here. assert.EqualValues(t, http.StatusOK, res.StatusCode) assert.NotEmpty(t, res.Header.Get("X-Kratos-Authenticated-Identity-Id")) diff --git a/spec/api.json b/spec/api.json index 1230c295cfdb..d4d3e5bd9f57 100644 --- a/spec/api.json +++ b/spec/api.json @@ -1156,10 +1156,6 @@ "description": "VerifiableAddressStatus must not exceed 16 characters as that is the limitation in the SQL Schema", "type": "string" }, - "identityVerifiableAddressType": { - "description": "VerifiableAddressType must not exceed 16 characters as that is the limitation in the SQL Schema", - "type": "string" - }, "identityWithCredentials": { "description": "Create Identity and Import Credentials", "properties": { @@ -1387,6 +1383,9 @@ "body": { "type": "string" }, + "channel": { + "type": "string" + }, "created_at": { "description": "CreatedAt is a helper struct field for gobuffalo.pop.", "format": "date-time", @@ -1417,7 +1416,7 @@ "type": "string" }, "template_type": { - "description": "\nrecovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub\nlogin_code_valid TypeLoginCodeValid\nregistration_code_valid TypeRegistrationCodeValid", + "description": "\nrecovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\nstub TypeTestStub\nlogin_code_valid TypeLoginCodeValid\nregistration_code_valid TypeRegistrationCodeValid", "enum": [ "recovery_invalid", "recovery_valid", @@ -1427,13 +1426,12 @@ "verification_valid", "verification_code_invalid", "verification_code_valid", - "otp", "stub", "login_code_valid", "registration_code_valid" ], "type": "string", - "x-go-enum-desc": "recovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub\nlogin_code_valid TypeLoginCodeValid\nregistration_code_valid TypeRegistrationCodeValid" + "x-go-enum-desc": "recovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\nstub TypeTestStub\nlogin_code_valid TypeLoginCodeValid\nregistration_code_valid TypeRegistrationCodeValid" }, "type": { "$ref": "#/components/schemas/courierMessageType" @@ -3253,7 +3251,13 @@ "$ref": "#/components/schemas/nullTime" }, "via": { - "$ref": "#/components/schemas/identityVerifiableAddressType" + "description": "The delivery method", + "enum": [ + "email", + "sms" + ], + "example": "email", + "type": "string" } }, "required": [ @@ -3577,9 +3581,9 @@ "x-go-enum-desc": " ConsistencyLevelUnset ConsistencyLevelUnset is the unset / default consistency level.\nstrong ConsistencyLevelStrong ConsistencyLevelStrong is the strong consistency level.\neventual ConsistencyLevelEventual ConsistencyLevelEventual is the eventual consistency level using follower read timestamps." }, { - "description": "IdsFilter is list of ids used to filter identities.\nIf this list is empty, then no filter will be applied.", + "description": "List of ids used to filter identities.\nIf this list is empty, then no filter will be applied.", "in": "query", - "name": "ids_filter", + "name": "ids", "schema": { "items": { "type": "string" diff --git a/spec/swagger.json b/spec/swagger.json index 1d1b2943cf13..83e6ea51c104 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -237,8 +237,8 @@ "items": { "type": "string" }, - "description": "IdsFilter is list of ids used to filter identities.\nIf this list is empty, then no filter will be applied.", - "name": "ids_filter", + "description": "List of ids used to filter identities.\nIf this list is empty, then no filter will be applied.", + "name": "ids", "in": "query" }, { @@ -4219,10 +4219,6 @@ "description": "VerifiableAddressStatus must not exceed 16 characters as that is the limitation in the SQL Schema", "type": "string" }, - "identityVerifiableAddressType": { - "description": "VerifiableAddressType must not exceed 16 characters as that is the limitation in the SQL Schema", - "type": "string" - }, "identityWithCredentials": { "description": "Create Identity and Import Credentials", "type": "object", @@ -4458,6 +4454,9 @@ "body": { "type": "string" }, + "channel": { + "type": "string" + }, "created_at": { "description": "CreatedAt is a helper struct field for gobuffalo.pop.", "type": "string", @@ -4488,7 +4487,7 @@ "type": "string" }, "template_type": { - "description": "\nrecovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub\nlogin_code_valid TypeLoginCodeValid\nregistration_code_valid TypeRegistrationCodeValid", + "description": "\nrecovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\nstub TypeTestStub\nlogin_code_valid TypeLoginCodeValid\nregistration_code_valid TypeRegistrationCodeValid", "type": "string", "enum": [ "recovery_invalid", @@ -4499,12 +4498,11 @@ "verification_valid", "verification_code_invalid", "verification_code_valid", - "otp", "stub", "login_code_valid", "registration_code_valid" ], - "x-go-enum-desc": "recovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\notp TypeOTP\nstub TypeTestStub\nlogin_code_valid TypeLoginCodeValid\nregistration_code_valid TypeRegistrationCodeValid" + "x-go-enum-desc": "recovery_invalid TypeRecoveryInvalid\nrecovery_valid TypeRecoveryValid\nrecovery_code_invalid TypeRecoveryCodeInvalid\nrecovery_code_valid TypeRecoveryCodeValid\nverification_invalid TypeVerificationInvalid\nverification_valid TypeVerificationValid\nverification_code_invalid TypeVerificationCodeInvalid\nverification_code_valid TypeVerificationCodeValid\nstub TypeTestStub\nlogin_code_valid TypeLoginCodeValid\nregistration_code_valid TypeRegistrationCodeValid" }, "type": { "$ref": "#/definitions/courierMessageType" @@ -6145,7 +6143,13 @@ "$ref": "#/definitions/nullTime" }, "via": { - "$ref": "#/definitions/identityVerifiableAddressType" + "description": "The delivery method", + "type": "string", + "enum": [ + "email", + "sms" + ], + "example": "email" } } }, diff --git a/test/e2e/cypress/integration/profiles/email/login/ui.spec.ts b/test/e2e/cypress/integration/profiles/email/login/ui.spec.ts index 8792dd14cb64..ff65ea6ca6b4 100644 --- a/test/e2e/cypress/integration/profiles/email/login/ui.spec.ts +++ b/test/e2e/cypress/integration/profiles/email/login/ui.spec.ts @@ -31,7 +31,7 @@ context("UI tests using the email profile", () => { it("should use the json schema titles", () => { cy.get(`${appPrefix(app)}input[name="identifier"]`) .parent() - .should("contain.text", "ID") + .should("contain.text", "Your E-Mail") cy.get('input[name="password"]') .parentsUntil("label") diff --git a/x/http_secure_redirect.go b/x/http_secure_redirect.go index cccbba6db2ec..9851abc0f53e 100644 --- a/x/http_secure_redirect.go +++ b/x/http_secure_redirect.go @@ -17,6 +17,8 @@ import ( "github.com/ory/x/stringsx" "github.com/ory/x/urlx" + "github.com/samber/lo" + "github.com/ory/kratos/driver/config" ) @@ -144,7 +146,9 @@ func SecureRedirectTo(r *http.Request, defaultReturnTo *url.URL, opts ...SecureR return nil, errors.WithStack(herodot.ErrBadRequest. WithID(text.ErrIDRedirectURLNotAllowed). WithReasonf("Requested return_to URL %q is not allowed.", returnTo). - WithDebugf("Allowed domains are: %v", o.allowlist)) + WithDebugf("Allowed domains are: %v", strings.Join(lo.Map(o.allowlist, func(u url.URL, _ int) string { + return u.String() + }), ", "))) } func SecureContentNegotiationRedirection(