// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package link_test import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" "github.com/ory/kratos/courier" "github.com/ory/kratos/driver/config" "github.com/ory/kratos/hydra" "github.com/ory/kratos/identity" "github.com/ory/kratos/pkg" "github.com/ory/kratos/pkg/testhelpers" "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/recovery" "github.com/ory/kratos/selfservice/flow/verification" "github.com/ory/kratos/selfservice/strategy/link" "github.com/ory/kratos/x" "github.com/ory/x/contextx" "github.com/ory/x/sqlxx" "github.com/ory/x/urlx" ) func TestManager(t *testing.T) { t.Parallel() ctx := t.Context() conf, reg := pkg.NewFastRegistryWithMocks(t) initViper(t, conf) ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://./stub/default.schema.json") ctx = contextx.WithConfigValues(ctx, map[string]any{ config.ViperKeyPublicBaseURL: "https://www.ory.com/", config.ViperKeyCourierSMTPURL: "smtp://foo@bar@dev.null/", config.ViperKeySelfServiceRecoveryNotifyUnknownRecipients: true, config.ViperKeySelfServiceVerificationNotifyUnknownRecipients: true, }) u := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.com/")} i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) i.Traits = identity.Traits(`{"email": "tracked@ory.sh"}`) require.NoError(t, reg.IdentityManager().Create(ctx, i)) for _, tc := range []struct { d string setupContext func(ctx context.Context) context.Context recoveryURL string verificationURL string }{{ d: "without BaseURL", setupContext: func(ctx context.Context) context.Context { return ctx }, recoveryURL: "https://www.ory.com/self-service/recovery?flow=", verificationURL: "https://www.ory.com/self-service/verification?flow=", }, { d: "with BaseURL", setupContext: func(ctx context.Context) context.Context { return x.WithBaseURL(ctx, urlx.ParseOrPanic("https://proxy.example.com/some/subpath/")) }, recoveryURL: "https://proxy.example.com/some/subpath/self-service/recovery?flow=", verificationURL: "https://proxy.example.com/some/subpath/self-service/verification?flow=", }} { ctx := tc.setupContext(ctx) t.Run("case="+tc.d, func(t *testing.T) { t.Run("method=SendRecoveryLink", func(t *testing.T) { s, _, err := reg.RecoveryStrategies(ctx).ActiveStrategies("link") require.NoError(t, err) f, err := recovery.NewFlow(reg, time.Hour, "", u.WithContext(ctx), s, flow.TypeBrowser) require.NoError(t, err) require.NoError(t, reg.RecoveryFlowPersister().CreateRecoveryFlow(ctx, f)) require.NoError(t, reg.LinkSender().SendRecoveryLink(ctx, f, "email", "tracked@ory.sh")) require.EqualError(t, reg.LinkSender().SendRecoveryLink(ctx, f, "email", "not-tracked@ory.sh"), link.ErrUnknownAddress.Error()) messages, err := reg.CourierPersister().NextMessages(ctx, 12) require.NoError(t, err) require.Len(t, messages, 2) assert.EqualValues(t, "tracked@ory.sh", messages[0].Recipient) assert.Contains(t, messages[0].Subject, "Recover access to your account") assert.Contains(t, messages[0].Body, tc.recoveryURL) assert.Contains(t, messages[0].Body, "token=") assert.Contains(t, messages[0].Body, "flow=") assert.EqualValues(t, "not-tracked@ory.sh", messages[1].Recipient) assert.Contains(t, messages[1].Subject, "Account access attempted") assert.NotContains(t, messages[1].Body, f.RequestURL+"self-service/recovery?flow=") assert.NotContains(t, messages[1].Body, "token=") assert.NotContains(t, messages[1].Body, "flow=") }) t.Run("method=SendRecoveryLink via HTTP", func(t *testing.T) { type requestBody struct { Recipient string RecoveryURL string `json:"recovery_url"` To string TemplateType string Subject string } messages := make(chan *requestBody, 2) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var message requestBody require.NoError(t, json.NewDecoder(r.Body).Decode(&message)) messages <- &message })) t.Cleanup(srv.Close) ctx = contextx.WithConfigValues(ctx, map[string]any{ config.ViperKeyCourierDeliveryStrategy: "http", config.ViperKeyCourierHTTPRequestConfig + ".url": srv.URL, }) s, _, err := reg.RecoveryStrategies(ctx).ActiveStrategies("link") require.NoError(t, err) f, err := recovery.NewFlow(reg, time.Hour, "", u.WithContext(ctx), s, flow.TypeBrowser) require.NoError(t, err) require.NoError(t, reg.RecoveryFlowPersister().CreateRecoveryFlow(ctx, f)) require.NoError(t, reg.LinkSender().SendRecoveryLink(ctx, f, "email", "tracked@ory.sh")) require.EqualError(t, reg.LinkSender().SendRecoveryLink(ctx, f, "email", "not-tracked@ory.sh"), link.ErrUnknownAddress.Error()) cour, err := reg.Courier(ctx) require.NoError(t, err) require.NoError(t, cour.DispatchQueue(ctx)) close(messages) require.Len(t, messages, 2) msg := <-messages assert.EqualValues(t, "tracked@ory.sh", msg.To) assert.Contains(t, msg.Subject, "Recover access to your account") assert.Contains(t, msg.RecoveryURL, tc.recoveryURL) msg = <-messages assert.EqualValues(t, "not-tracked@ory.sh", msg.To) assert.Contains(t, msg.Subject, "Account access attempted") assert.NotContains(t, msg.RecoveryURL, tc.recoveryURL) }) t.Run("method=SendVerificationLink", func(t *testing.T) { strategies, _, err := reg.GetActiveVerificationStrategies(ctx) require.NoError(t, err) f, err := verification.NewFlow(reg, time.Hour, "", u.WithContext(ctx), strategies, flow.TypeBrowser) require.NoError(t, err) require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) require.NoError(t, reg.LinkSender().SendVerificationLink(ctx, f, "email", "tracked@ory.sh")) require.EqualError(t, reg.LinkSender().SendVerificationLink(ctx, f, "email", "not-tracked@ory.sh"), link.ErrUnknownAddress.Error()) messages, err := reg.CourierPersister().NextMessages(ctx, 12) require.NoError(t, err) require.Len(t, messages, 2) assert.EqualValues(t, "tracked@ory.sh", messages[0].Recipient) assert.Contains(t, messages[0].Subject, "Please verify") assert.Contains(t, messages[0].Body, tc.verificationURL) assert.Contains(t, messages[0].Body, "token=") assert.Contains(t, messages[0].Body, "flow=") assert.EqualValues(t, "not-tracked@ory.sh", messages[1].Recipient) assert.Contains(t, messages[1].Subject, "tried to verify") assert.NotContains(t, messages[1].Body, tc.verificationURL) address, err := reg.IdentityPool().FindVerifiableAddressByValue(ctx, identity.AddressTypeEmail, "tracked@ory.sh") require.NoError(t, err) assert.EqualValues(t, identity.VerifiableAddressStatusSent, address.Status) }) }) } t.Run("method=SendVerificationLink with OAuth2 login challenge", func(t *testing.T) { reg.SetHydra(hydra.NewFake()) strategies, _, err := reg.GetActiveVerificationStrategies(ctx) require.NoError(t, err) f, err := verification.NewFlow(reg, time.Hour, "", u.WithContext(ctx), strategies, flow.TypeBrowser) require.NoError(t, err) f.OAuth2LoginChallenge = sqlxx.NullString(hydra.FakeValidLoginChallenge) require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) require.NoError(t, reg.LinkSender().SendVerificationLink(ctx, f, "email", "tracked@ory.sh")) messages, err := reg.CourierPersister().NextMessages(ctx, 12) require.NoError(t, err) require.Len(t, messages, 1) templateData := messages[0].TemplateData assert.Equal(t, hydra.FakeValidLoginChallenge, gjson.GetBytes(templateData, "oauth2_login_request.challenge").String()) assert.Equal(t, hydra.FakeClientID, gjson.GetBytes(templateData, "oauth2_login_request.client.client_id").String()) }) t.Run("case=should be able to disable invalid email dispatch", func(t *testing.T) { for _, tc := range []struct { flow string send func(t *testing.T) configKey string }{ { flow: "recovery", configKey: config.ViperKeySelfServiceRecoveryNotifyUnknownRecipients, send: func(t *testing.T) { s, _, err := reg.RecoveryStrategies(ctx).ActiveStrategies("link") require.NoError(t, err) f, err := recovery.NewFlow(reg, time.Hour, "", u.WithContext(ctx), s, flow.TypeBrowser) require.NoError(t, err) require.NoError(t, reg.RecoveryFlowPersister().CreateRecoveryFlow(ctx, f)) err = reg.LinkSender().SendRecoveryLink(ctx, f, "email", "not-tracked@ory.sh") require.ErrorIs(t, err, link.ErrUnknownAddress) }, }, { flow: "verification", configKey: config.ViperKeySelfServiceVerificationNotifyUnknownRecipients, send: func(t *testing.T) { s, _, err := reg.VerificationStrategies(ctx).ActiveStrategies("link") require.NoError(t, err) f, err := verification.NewFlow(reg, time.Hour, "", u.WithContext(ctx), s, flow.TypeBrowser) require.NoError(t, err) require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f)) err = reg.LinkSender().SendVerificationLink(ctx, f, "email", "not-tracked@ory.sh") require.ErrorIs(t, err, link.ErrUnknownAddress) }, }, } { t.Run("strategy="+tc.flow, func(t *testing.T) { ctx = contextx.WithConfigValue(ctx, tc.configKey, false) tc.send(t) messages, err := reg.CourierPersister().NextMessages(ctx, 0) require.ErrorIs(t, err, courier.ErrQueueEmpty) require.Len(t, messages, 0) }) } }) }