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
541 lines
16 KiB
Go
541 lines
16 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package identity
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"net"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/ory/jsonschema/v3"
|
|
_ "github.com/ory/jsonschema/v3/fileloader"
|
|
"github.com/ory/kratos/schema"
|
|
"github.com/ory/kratos/x"
|
|
)
|
|
|
|
const (
|
|
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"
|
|
)
|
|
|
|
func TestSchemaExtensionVerification(t *testing.T) {
|
|
net.IP{}.IsPrivate()
|
|
t.Run("address verification", func(t *testing.T) {
|
|
iid := x.NewUUID()
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
schema string
|
|
expectErr error
|
|
doc string
|
|
existing []VerifiableAddress
|
|
expect []VerifiableAddress
|
|
}{
|
|
{
|
|
name: "email:must create new address",
|
|
schema: emailSchemaPath,
|
|
doc: `{"username":"foo@ory.sh"}`,
|
|
expect: []VerifiableAddress{
|
|
{
|
|
Value: "foo@ory.sh",
|
|
Verified: false,
|
|
Status: VerifiableAddressStatusPending,
|
|
Via: AddressTypeEmail,
|
|
IdentityID: iid,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "email:must create new address because new and existing doesn't match",
|
|
schema: emailSchemaPath,
|
|
doc: `{"username":"foo@ory.sh"}`,
|
|
existing: []VerifiableAddress{
|
|
{
|
|
Value: "bar@ory.sh",
|
|
Verified: false,
|
|
Status: VerifiableAddressStatusPending,
|
|
Via: AddressTypeEmail,
|
|
IdentityID: iid,
|
|
},
|
|
},
|
|
expect: []VerifiableAddress{
|
|
{
|
|
Value: "foo@ory.sh",
|
|
Verified: false,
|
|
Status: VerifiableAddressStatusPending,
|
|
Via: AddressTypeEmail,
|
|
IdentityID: iid,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "email:must find existing address in case of match",
|
|
schema: emailSchemaPath,
|
|
doc: `{"emails":["baz@ory.sh","foo@ory.sh"]}`,
|
|
existing: []VerifiableAddress{
|
|
{
|
|
Value: "foo@ory.sh",
|
|
Verified: true,
|
|
Status: VerifiableAddressStatusCompleted,
|
|
Via: AddressTypeEmail,
|
|
IdentityID: iid,
|
|
},
|
|
{
|
|
Value: "bar@ory.sh",
|
|
Verified: true,
|
|
Status: VerifiableAddressStatusCompleted,
|
|
Via: AddressTypeEmail,
|
|
IdentityID: iid,
|
|
},
|
|
},
|
|
expect: []VerifiableAddress{
|
|
{
|
|
Value: "foo@ory.sh",
|
|
Verified: true,
|
|
Status: VerifiableAddressStatusCompleted,
|
|
Via: AddressTypeEmail,
|
|
IdentityID: iid,
|
|
},
|
|
{
|
|
Value: "baz@ory.sh",
|
|
Verified: false,
|
|
Status: VerifiableAddressStatusPending,
|
|
Via: AddressTypeEmail,
|
|
IdentityID: iid,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "email:must return only one address in case of duplication",
|
|
schema: emailSchemaPath,
|
|
doc: `{"emails":["foo@ory.sh","foo@ory.sh","baz@ory.sh"]}`,
|
|
existing: []VerifiableAddress{
|
|
{
|
|
Value: "foo@ory.sh",
|
|
Verified: true,
|
|
Status: VerifiableAddressStatusCompleted,
|
|
Via: AddressTypeEmail,
|
|
IdentityID: iid,
|
|
},
|
|
{
|
|
Value: "bar@ory.sh",
|
|
Verified: true,
|
|
Status: VerifiableAddressStatusCompleted,
|
|
Via: AddressTypeEmail,
|
|
IdentityID: iid,
|
|
},
|
|
},
|
|
expect: []VerifiableAddress{
|
|
{
|
|
Value: "foo@ory.sh",
|
|
Verified: true,
|
|
Status: VerifiableAddressStatusCompleted,
|
|
Via: AddressTypeEmail,
|
|
IdentityID: iid,
|
|
},
|
|
{
|
|
Value: "baz@ory.sh",
|
|
Verified: false,
|
|
Status: VerifiableAddressStatusPending,
|
|
Via: AddressTypeEmail,
|
|
IdentityID: iid,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "email:must return error for malformed input",
|
|
schema: emailSchemaPath,
|
|
doc: `{"emails":["foo@ory.sh","bar@ory.sh"], "username": "foobar"}`,
|
|
expectErr: errors.New("I[#/username] S[#/properties/username/format] \"foobar\" is not valid \"email\""),
|
|
},
|
|
{
|
|
name: "email:must merge email addresses from multiple attributes",
|
|
schema: emailSchemaPath,
|
|
doc: `{"emails":["foo@ory.sh","bar@ory.sh","bar@ory.sh"], "username": "foobar@ory.sh"}`,
|
|
expect: []VerifiableAddress{
|
|
{
|
|
Value: "foo@ory.sh",
|
|
Verified: false,
|
|
Status: VerifiableAddressStatusPending,
|
|
Via: AddressTypeEmail,
|
|
IdentityID: iid,
|
|
},
|
|
{
|
|
Value: "bar@ory.sh",
|
|
Verified: false,
|
|
Status: VerifiableAddressStatusPending,
|
|
Via: AddressTypeEmail,
|
|
IdentityID: iid,
|
|
},
|
|
{
|
|
Value: "foobar@ory.sh",
|
|
Verified: false,
|
|
Status: VerifiableAddressStatusPending,
|
|
Via: AddressTypeEmail,
|
|
IdentityID: iid,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "phone:must create new address",
|
|
schema: phoneSchemaPath,
|
|
doc: `{"username":"+18004444444"}`,
|
|
expect: []VerifiableAddress{
|
|
{
|
|
Value: "+18004444444",
|
|
Verified: false,
|
|
Status: VerifiableAddressStatusPending,
|
|
Via: ChannelTypeSMS,
|
|
IdentityID: iid,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "phone:must create new address because new and existing doesn't match",
|
|
schema: phoneSchemaPath,
|
|
doc: `{"username":"+18004444444"}`,
|
|
existing: []VerifiableAddress{
|
|
{
|
|
Value: "+442087599036",
|
|
Verified: false,
|
|
Status: VerifiableAddressStatusPending,
|
|
Via: ChannelTypeSMS,
|
|
IdentityID: iid,
|
|
},
|
|
},
|
|
expect: []VerifiableAddress{
|
|
{
|
|
Value: "+18004444444",
|
|
Verified: false,
|
|
Status: VerifiableAddressStatusPending,
|
|
Via: ChannelTypeSMS,
|
|
IdentityID: iid,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "phone:must find existing addresses in case of match",
|
|
schema: phoneSchemaPath,
|
|
doc: `{"phones":["+18004444444","+442087599036"]}`,
|
|
existing: []VerifiableAddress{
|
|
{
|
|
Value: "+442087599036",
|
|
Verified: true,
|
|
Status: VerifiableAddressStatusCompleted,
|
|
Via: ChannelTypeSMS,
|
|
IdentityID: iid,
|
|
},
|
|
{
|
|
Value: "+380634872774",
|
|
Verified: true,
|
|
Status: VerifiableAddressStatusCompleted,
|
|
Via: ChannelTypeSMS,
|
|
IdentityID: iid,
|
|
},
|
|
},
|
|
expect: []VerifiableAddress{
|
|
{
|
|
Value: "+442087599036",
|
|
Verified: true,
|
|
Status: VerifiableAddressStatusCompleted,
|
|
Via: ChannelTypeSMS,
|
|
IdentityID: iid,
|
|
},
|
|
{
|
|
Value: "+18004444444",
|
|
Verified: false,
|
|
Status: VerifiableAddressStatusPending,
|
|
Via: ChannelTypeSMS,
|
|
IdentityID: iid,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "phone:must return only one address in case of duplication",
|
|
schema: phoneSchemaPath,
|
|
doc: `{"phones": ["+18004444444","+18004444444","+442087599036"]}`,
|
|
existing: []VerifiableAddress{
|
|
{
|
|
Value: "+18004444444",
|
|
Verified: false,
|
|
Status: VerifiableAddressStatusPending,
|
|
Via: ChannelTypeSMS,
|
|
IdentityID: iid,
|
|
},
|
|
{
|
|
Value: "+380634872774",
|
|
Verified: true,
|
|
Status: VerifiableAddressStatusCompleted,
|
|
Via: ChannelTypeSMS,
|
|
IdentityID: iid,
|
|
},
|
|
},
|
|
expect: []VerifiableAddress{
|
|
{
|
|
Value: "+18004444444",
|
|
Verified: false,
|
|
Status: VerifiableAddressStatusPending,
|
|
Via: ChannelTypeSMS,
|
|
IdentityID: iid,
|
|
},
|
|
{
|
|
Value: "+442087599036",
|
|
Verified: false,
|
|
Status: VerifiableAddressStatusPending,
|
|
Via: ChannelTypeSMS,
|
|
IdentityID: iid,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "phone:must merge phones from multiple attributes",
|
|
schema: phoneSchemaPath,
|
|
doc: `{"phones": ["+18004444444","+18004444444","+442087599036"], "username": "+380634872774"}`,
|
|
expect: []VerifiableAddress{
|
|
{
|
|
Value: "+18004444444",
|
|
Verified: false,
|
|
Status: VerifiableAddressStatusPending,
|
|
Via: ChannelTypeSMS,
|
|
IdentityID: iid,
|
|
},
|
|
{
|
|
Value: "+442087599036",
|
|
Verified: false,
|
|
Status: VerifiableAddressStatusPending,
|
|
Via: ChannelTypeSMS,
|
|
IdentityID: iid,
|
|
},
|
|
{
|
|
Value: "+380634872774",
|
|
Verified: false,
|
|
Status: VerifiableAddressStatusPending,
|
|
Via: ChannelTypeSMS,
|
|
IdentityID: iid,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
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] validation failed\n I[#/phones/2] S[#/properties/phones/items/format] \"12112112\" is not valid \"tel\"\n I[#/phones/2] S[#/properties/phones/items/format] \"12112112\" can not be normalized for format \"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,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// see https://github.com/ory/kratos/issues/3933
|
|
name: "phone:should parse +16453331111",
|
|
schema: phoneSchemaPath,
|
|
doc: `{"phones":["+16453331111"]}`,
|
|
expect: []VerifiableAddress{
|
|
{
|
|
Value: "+16453331111",
|
|
Verified: false,
|
|
Status: VerifiableAddressStatusPending,
|
|
Via: ChannelTypeSMS,
|
|
IdentityID: iid,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "phone:should normalize +1 (645) 333-1111 to +16453331111",
|
|
schema: phoneSchemaPath,
|
|
doc: `{"phones":["+1 (645) 333-1111"]}`,
|
|
expect: []VerifiableAddress{
|
|
{
|
|
Value: "+16453331111",
|
|
Verified: false,
|
|
Status: VerifiableAddressStatusPending,
|
|
Via: ChannelTypeSMS,
|
|
IdentityID: iid,
|
|
},
|
|
},
|
|
},
|
|
} {
|
|
t.Run(fmt.Sprintf("case=%v", tc.name), func(t *testing.T) {
|
|
id := &Identity{ID: iid, VerifiableAddresses: tc.existing}
|
|
|
|
c := jsonschema.NewCompiler()
|
|
|
|
runner, err := schema.NewExtensionRunner(t.Context())
|
|
require.NoError(t, err)
|
|
|
|
e := NewSchemaExtensionVerification(id, time.Minute)
|
|
runner.AddRunner(e).Register(c)
|
|
|
|
err = c.MustCompile(t.Context(), tc.schema).Validate(bytes.NewBufferString(tc.doc))
|
|
if tc.expectErr != nil {
|
|
require.EqualError(t, err, tc.expectErr.Error())
|
|
return
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
require.NoError(t, e.Finish())
|
|
|
|
addresses := id.VerifiableAddresses
|
|
require.Len(t, addresses, len(tc.expect))
|
|
|
|
mustContainAddress(t, tc.expect, addresses)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func mustContainAddress(t *testing.T, expected, actual []VerifiableAddress) {
|
|
for _, act := range actual {
|
|
var found bool
|
|
for _, expect := range expected {
|
|
if reflect.DeepEqual(act, expect) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "%+v not in %+v", act, expected)
|
|
}
|
|
}
|
|
|
|
// TestSchemaExtensionVerification_PreservesVerifiedWhenExistingValueIsNotNormalized
|
|
// is a regression test for the bug where identities created before phone
|
|
// normalization (#11283) lose their verified status on re-validation because
|
|
// the persisted VerifiableAddress.Value is in raw form but the extension now
|
|
// normalizes the trait to E.164 before comparing.
|
|
//
|
|
// Invariant we rely on: a persisted phone trait that was verified before must
|
|
// stay verified after re-validation, even when the persisted
|
|
// VerifiableAddress.Value is not in E.164 form.
|
|
func TestSchemaExtensionVerification_PreservesVerifiedWhenExistingValueIsNotNormalized(t *testing.T) {
|
|
iid := x.NewUUID()
|
|
|
|
// Trait and existing VerifiableAddress both hold the pre-normalization
|
|
// raw value (contains an extra leading zero from the national trunk
|
|
// prefix). Before #11283, the extension trimmed only, so this value was
|
|
// persisted verbatim. After #11283, the same trait normalizes to
|
|
// "+4917667111638" -- a value that does not match the persisted
|
|
// VerifiableAddress.Value.
|
|
rawPhone := "+49 0176 67111638"
|
|
id := &Identity{
|
|
ID: iid,
|
|
VerifiableAddresses: []VerifiableAddress{{
|
|
Value: rawPhone,
|
|
Verified: true,
|
|
Status: VerifiableAddressStatusCompleted,
|
|
Via: ChannelTypeSMS,
|
|
IdentityID: iid,
|
|
}},
|
|
}
|
|
|
|
c := jsonschema.NewCompiler()
|
|
runner, err := schema.NewExtensionRunner(t.Context())
|
|
require.NoError(t, err)
|
|
|
|
e := NewSchemaExtensionVerification(id, time.Minute)
|
|
runner.AddRunner(e).Register(c)
|
|
|
|
err = c.MustCompile(t.Context(), phoneSchemaPath).
|
|
Validate(bytes.NewBufferString(fmt.Sprintf(`{"username":%q}`, rawPhone)))
|
|
require.NoError(t, err)
|
|
require.NoError(t, e.Finish())
|
|
|
|
require.Len(t, id.VerifiableAddresses, 1)
|
|
assert.True(t, id.VerifiableAddresses[0].Verified,
|
|
"persisted phone must remain verified after re-validation; got %+v",
|
|
id.VerifiableAddresses[0])
|
|
}
|
|
|
|
func TestMergeVerifiableAddresses(t *testing.T) {
|
|
for _, tt := range []struct {
|
|
name string
|
|
base, overrides, expected []VerifiableAddress
|
|
}{
|
|
{
|
|
name: "empty base",
|
|
base: []VerifiableAddress{},
|
|
overrides: []VerifiableAddress{{
|
|
Value: "override@ory.sh",
|
|
Via: "email",
|
|
}},
|
|
expected: []VerifiableAddress{},
|
|
}, {
|
|
name: "no overlap",
|
|
base: []VerifiableAddress{{
|
|
Value: "base@ory.sh",
|
|
Via: "email",
|
|
}},
|
|
overrides: []VerifiableAddress{{
|
|
Value: "override@ory.sh",
|
|
Via: "email",
|
|
}},
|
|
expected: []VerifiableAddress{{
|
|
Value: "base@ory.sh",
|
|
Via: "email",
|
|
}},
|
|
}, {
|
|
name: "overrides",
|
|
base: []VerifiableAddress{
|
|
{Value: "base@ory.sh", Via: "email"},
|
|
{Value: "common-1-is-overwritten@ory.sh", Via: "email"},
|
|
{Value: "common-2-is-ignored@ory.sh", Via: "no match"},
|
|
},
|
|
overrides: []VerifiableAddress{
|
|
{Value: "common-1-is-overwritten@ory.sh", Via: "email", Verified: true, Status: VerifiableAddressStatusCompleted},
|
|
{Value: "common-2-is-ignored@ory.sh", Via: "wrong via"},
|
|
{Value: "override-only-is-ignored@ory.sh", Via: "email"},
|
|
},
|
|
expected: []VerifiableAddress{
|
|
{Value: "base@ory.sh", Via: "email"},
|
|
{Value: "common-1-is-overwritten@ory.sh", Via: "email", Verified: true, Status: VerifiableAddressStatusCompleted},
|
|
{Value: "common-2-is-ignored@ory.sh", Via: "no match"},
|
|
},
|
|
},
|
|
} {
|
|
t.Run("case="+tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.expected, merge(tt.base, tt.overrides))
|
|
})
|
|
}
|
|
}
|