Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
281 lines
11 KiB
Go
281 lines
11 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package hook_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gofrs/uuid"
|
|
"github.com/tidwall/gjson"
|
|
|
|
"github.com/ory/kratos/courier"
|
|
"github.com/ory/kratos/pkg/testhelpers"
|
|
"github.com/ory/kratos/ui/node"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/ory/kratos/driver/config"
|
|
"github.com/ory/kratos/identity"
|
|
"github.com/ory/kratos/pkg"
|
|
"github.com/ory/kratos/selfservice/flow"
|
|
"github.com/ory/kratos/selfservice/flow/login"
|
|
"github.com/ory/kratos/selfservice/flow/registration"
|
|
"github.com/ory/kratos/selfservice/flow/settings"
|
|
"github.com/ory/kratos/selfservice/hook"
|
|
"github.com/ory/kratos/session"
|
|
"github.com/ory/kratos/text"
|
|
"github.com/ory/kratos/x"
|
|
"github.com/ory/x/sqlxx"
|
|
"github.com/ory/x/urlx"
|
|
)
|
|
|
|
func TestVerifier(t *testing.T) {
|
|
ctx := context.Background()
|
|
u := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.com/")}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
execHook func(h *hook.Verifier, i *identity.Identity, f flow.Flow) error
|
|
|
|
originalFlow func() interface {
|
|
flow.InternalContexter
|
|
flow.FlowWithContinueWith
|
|
}
|
|
}{
|
|
{
|
|
name: "login",
|
|
execHook: func(h *hook.Verifier, i *identity.Identity, f flow.Flow) error {
|
|
return h.ExecuteLoginPostHook(
|
|
httptest.NewRecorder(), u, node.CodeGroup, f.(*login.Flow), &session.Session{ID: x.NewUUID(), Identity: i})
|
|
},
|
|
originalFlow: func() interface {
|
|
flow.InternalContexter
|
|
flow.FlowWithContinueWith
|
|
} {
|
|
return &login.Flow{RequestURL: "http://foo.com/login", RequestedAAL: "aal1"}
|
|
},
|
|
},
|
|
{
|
|
name: "registration",
|
|
execHook: func(h *hook.Verifier, i *identity.Identity, f flow.Flow) error {
|
|
return h.ExecutePostRegistrationPostPersistHook(
|
|
httptest.NewRecorder(), u, f.(*registration.Flow), &session.Session{ID: x.NewUUID(), Identity: i})
|
|
},
|
|
originalFlow: func() interface {
|
|
flow.InternalContexter
|
|
flow.FlowWithContinueWith
|
|
} {
|
|
return ®istration.Flow{RequestURL: "http://foo.com/registration?after_verification_return_to=verification_callback"}
|
|
},
|
|
},
|
|
} {
|
|
t.Run("flow="+tc.name, func(t *testing.T) {
|
|
for _, enabled := range []bool{true, false} {
|
|
t.Run(fmt.Sprintf("legacy flag=%v", enabled), func(t *testing.T) {
|
|
t.Run("case=should send out emails for unverified addresses", func(t *testing.T) {
|
|
originalFlow := tc.originalFlow()
|
|
conf, reg := pkg.NewFastRegistryWithMocks(t)
|
|
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/verify.schema.json")
|
|
conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.com/")
|
|
conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/")
|
|
conf.MustSet(ctx, config.ViperKeyUseLegacyShowVerificationUI, enabled)
|
|
|
|
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
|
i.Traits = identity.Traits(`{"emails":["foo@ory.sh","bar@ory.sh"]}`)
|
|
require.NoError(t, reg.IdentityManager().Create(context.Background(), i))
|
|
|
|
h := hook.NewVerifier(reg)
|
|
require.NoError(t, tc.execHook(h, i, originalFlow))
|
|
assert.Lenf(t, gjson.GetBytes(originalFlow.GetInternalContext(), hook.InternalContextRegistrationVerificationFlow).Array(), 1, "%s", originalFlow.GetInternalContext())
|
|
|
|
var verificationID uuid.UUID
|
|
if enabled {
|
|
assert.Lenf(t, originalFlow.ContinueWith(), 2, "%#v", originalFlow.ContinueWith())
|
|
assertContinueWithAddresses(t, originalFlow.ContinueWith(), []string{"foo@ory.sh", "bar@ory.sh"})
|
|
vf := originalFlow.ContinueWith()[0]
|
|
assert.IsType(t, &flow.ContinueWithVerificationUI{}, vf)
|
|
verificationID = vf.(*flow.ContinueWithVerificationUI).Flow.ID
|
|
} else {
|
|
assert.Lenf(t, originalFlow.ContinueWith(), 0, "%#v", originalFlow.ContinueWith())
|
|
verificationID = uuid.FromStringOrNil(gjson.GetBytes(originalFlow.GetInternalContext(), hook.InternalContextRegistrationVerificationFlow+".id").String())
|
|
require.NotEqual(t, uuid.Nil, verificationID, "%s", originalFlow.GetInternalContext())
|
|
}
|
|
|
|
expectedVerificationFlow, err := reg.VerificationFlowPersister().GetVerificationFlow(ctx, verificationID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, expectedVerificationFlow.State, flow.StateEmailSent)
|
|
require.NotEmpty(t, expectedVerificationFlow.SessionID.UUID)
|
|
require.NotEmpty(t, expectedVerificationFlow.IdentityID.UUID)
|
|
require.NotNil(t, expectedVerificationFlow.UI.Nodes.Find("email"))
|
|
require.Len(t, expectedVerificationFlow.UI.Messages, 1)
|
|
require.Equal(t, text.InfoSelfServiceVerificationEmailWithCodeSent, expectedVerificationFlow.UI.Messages[0].ID)
|
|
|
|
messages, err := reg.CourierPersister().NextMessages(context.Background(), 12)
|
|
require.NoError(t, err)
|
|
if enabled {
|
|
require.Len(t, messages, 2)
|
|
} else {
|
|
require.Len(t, messages, 1)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
t.Run("case should skip already verified addresses", func(t *testing.T) {
|
|
t.Parallel()
|
|
originalFlow := tc.originalFlow()
|
|
conf, reg := pkg.NewFastRegistryWithMocks(t)
|
|
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/verify.schema.json")
|
|
conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.com/")
|
|
conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/")
|
|
h := hook.NewVerifier(reg)
|
|
|
|
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
|
i.Traits = identity.Traits(`{"emails":["foo@ory.sh","bar@ory.sh"]}`)
|
|
require.NoError(t, reg.IdentityManager().Create(context.Background(), i))
|
|
for _, address := range i.VerifiableAddresses {
|
|
address.Verified = true
|
|
address.VerifiedAt = new(sqlxx.NullTime(time.Now()))
|
|
address.Status = identity.VerifiableAddressStatusCompleted
|
|
require.NoError(t, reg.Persister().UpdateVerifiableAddress(context.Background(), &address))
|
|
}
|
|
i, err := reg.PrivilegedIdentityPool().GetIdentity(ctx, i.ID, identity.ExpandDefault)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, tc.execHook(h, i, originalFlow))
|
|
assert.Empty(t, originalFlow.ContinueWith(), "%#ßv", originalFlow.ContinueWith())
|
|
})
|
|
})
|
|
}
|
|
|
|
t.Run("flow=login/case=does not run if aal is not 1", func(t *testing.T) {
|
|
t.Parallel()
|
|
_, reg := pkg.NewFastRegistryWithMocks(t)
|
|
|
|
h := hook.NewVerifier(reg)
|
|
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
|
f := &login.Flow{RequestedAAL: "aal2"}
|
|
require.NoError(t, h.ExecuteLoginPostHook(httptest.NewRecorder(), u, node.CodeGroup, f, &session.Session{ID: x.NewUUID(), Identity: i}))
|
|
|
|
messages, err := reg.CourierPersister().NextMessages(context.Background(), 12)
|
|
require.EqualError(t, err, "queue is empty")
|
|
require.Len(t, messages, 0)
|
|
})
|
|
|
|
t.Run("name=settings", func(t *testing.T) {
|
|
t.Parallel()
|
|
conf, reg := pkg.NewFastRegistryWithMocks(t)
|
|
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/verify.schema.json")
|
|
conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.com/")
|
|
conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/")
|
|
conf.MustSet(ctx, config.ViperKeyUseLegacyShowVerificationUI, true)
|
|
|
|
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
|
i.Traits = identity.Traits(`{"emails":["foo@ory.sh","bar@ory.sh","baz@ory.sh"]}`)
|
|
require.NoError(t, reg.IdentityManager().Create(context.Background(), i))
|
|
|
|
actual, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.AddressTypeEmail, "foo@ory.sh")
|
|
require.NoError(t, err)
|
|
assert.EqualValues(t, "foo@ory.sh", actual.Value)
|
|
|
|
actual, err = reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.AddressTypeEmail, "bar@ory.sh")
|
|
require.NoError(t, err)
|
|
assert.EqualValues(t, "bar@ory.sh", actual.Value)
|
|
|
|
actual, err = reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.AddressTypeEmail, "baz@ory.sh")
|
|
require.NoError(t, err)
|
|
assert.EqualValues(t, "baz@ory.sh", actual.Value)
|
|
|
|
verifiedAt := sqlxx.NullTime(time.Now())
|
|
actual.Status = identity.VerifiableAddressStatusCompleted
|
|
actual.Verified = true
|
|
actual.VerifiedAt = &verifiedAt
|
|
require.NoError(t, reg.PrivilegedIdentityPool().UpdateVerifiableAddress(context.Background(), actual))
|
|
|
|
i, err = reg.IdentityPool().GetIdentity(context.Background(), i.ID, identity.ExpandDefault)
|
|
require.NoError(t, err)
|
|
|
|
originalFlow := &settings.Flow{RequestURL: "http://foo.com/settings?after_verification_return_to=verification_callback"}
|
|
|
|
h := hook.NewVerifier(reg)
|
|
require.NoError(t, h.ExecuteSettingsPostPersistHook(
|
|
httptest.NewRecorder(), u, settings.PostHookPostPersistExecutorParams{
|
|
Flow: originalFlow,
|
|
Updated: i,
|
|
Previous: i,
|
|
Session: &session.Session{ID: x.NewUUID(), Identity: i},
|
|
}))
|
|
assert.Lenf(t, originalFlow.ContinueWith(), 2, "%#ßv", originalFlow.ContinueWith())
|
|
assertContinueWithAddresses(t, originalFlow.ContinueWith(), []string{"foo@ory.sh", "bar@ory.sh"})
|
|
vf := originalFlow.ContinueWith()[0]
|
|
assert.IsType(t, &flow.ContinueWithVerificationUI{}, vf)
|
|
fView := vf.(*flow.ContinueWithVerificationUI).Flow
|
|
|
|
expectedVerificationFlow, err := reg.VerificationFlowPersister().GetVerificationFlow(ctx, fView.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, expectedVerificationFlow.State, flow.StateEmailSent)
|
|
|
|
messages, err := reg.CourierPersister().NextMessages(context.Background(), 12)
|
|
require.NoError(t, err)
|
|
require.Len(t, messages, 2)
|
|
|
|
recipients := make([]string, len(messages))
|
|
for k, m := range messages {
|
|
recipients[k] = m.Recipient
|
|
}
|
|
|
|
assert.Contains(t, recipients, "foo@ory.sh")
|
|
assert.Contains(t, recipients, "bar@ory.sh")
|
|
// Email to baz@ory.sh is skipped because it is verified already.
|
|
assert.NotContains(t, recipients, "baz@ory.sh")
|
|
|
|
// these addresses will be marked as sent and won't be sent again by the settings hook
|
|
address1, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.AddressTypeEmail, "foo@ory.sh")
|
|
require.NoError(t, err)
|
|
assert.EqualValues(t, identity.VerifiableAddressStatusSent, address1.Status)
|
|
address2, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.AddressTypeEmail, "bar@ory.sh")
|
|
require.NoError(t, err)
|
|
assert.EqualValues(t, identity.VerifiableAddressStatusSent, address2.Status)
|
|
|
|
originalFlow = &settings.Flow{RequestURL: "http://foo.com/settings?after_verification_return_to=verification_callback"}
|
|
|
|
require.NoError(t, h.ExecuteSettingsPostPersistHook(
|
|
httptest.NewRecorder(), u, settings.PostHookPostPersistExecutorParams{
|
|
Flow: originalFlow,
|
|
Updated: i,
|
|
Previous: i,
|
|
Session: &session.Session{ID: x.NewUUID(), Identity: i},
|
|
}))
|
|
|
|
assert.Emptyf(t, originalFlow.ContinueWith(), "%+v", originalFlow.ContinueWith())
|
|
|
|
require.NoError(t, err)
|
|
messages, err = reg.CourierPersister().NextMessages(context.Background(), 12)
|
|
require.EqualError(t, err, courier.ErrQueueEmpty.Error())
|
|
assert.Len(t, messages, 0)
|
|
})
|
|
}
|
|
|
|
func assertContinueWithAddresses(t *testing.T, cs []flow.ContinueWith, addresses []string) {
|
|
t.Helper()
|
|
|
|
require.Len(t, cs, len(addresses))
|
|
|
|
e := make([]string, len(cs))
|
|
|
|
for i, c := range cs {
|
|
require.IsType(t, &flow.ContinueWithVerificationUI{}, c)
|
|
fView := c.(*flow.ContinueWithVerificationUI).Flow
|
|
e[i] = fView.VerifiableAddress
|
|
}
|
|
|
|
require.ElementsMatch(t, addresses, e)
|
|
}
|