snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
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
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
This commit is contained in:
540
identity/extension_verification_test.go
Normal file
540
identity/extension_verification_test.go
Normal file
@@ -0,0 +1,540 @@
|
||||
// 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))
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user