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:
376
identity/extension_recovery_test.go
Normal file
376
identity/extension_recovery_test.go
Normal file
@@ -0,0 +1,376 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package identity
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/jsonschema/v3"
|
||||
_ "github.com/ory/jsonschema/v3/fileloader"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
|
||||
"github.com/ory/kratos/schema"
|
||||
"github.com/ory/kratos/x"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSchemaExtensionRecovery(t *testing.T) {
|
||||
iid := x.NewUUID()
|
||||
breakGlassOrgID := uuid.NullUUID{UUID: x.NewUUID(), Valid: true}
|
||||
for k, tc := range []struct {
|
||||
expectErr error
|
||||
schema string
|
||||
doc string
|
||||
expect []RecoveryAddress
|
||||
existing []RecoveryAddress
|
||||
description string
|
||||
}{
|
||||
{
|
||||
description: "valid email, no existing",
|
||||
doc: `{"username":"foo@ory.sh"}`,
|
||||
schema: "file://./stub/extension/recovery/email.schema.json",
|
||||
expect: []RecoveryAddress{
|
||||
{
|
||||
Value: "foo@ory.sh",
|
||||
Via: AddressTypeEmail,
|
||||
IdentityID: iid,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "valid email, some existing, no overlap",
|
||||
doc: `{"username":"foo@ory.sh"}`,
|
||||
schema: "file://./stub/extension/recovery/email.schema.json",
|
||||
expect: []RecoveryAddress{
|
||||
{
|
||||
Value: "foo@ory.sh",
|
||||
Via: AddressTypeEmail,
|
||||
IdentityID: iid,
|
||||
},
|
||||
},
|
||||
existing: []RecoveryAddress{
|
||||
{
|
||||
Value: "bar@ory.sh",
|
||||
Via: AddressTypeEmail,
|
||||
IdentityID: iid,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "valid emails, some existing, overlap",
|
||||
doc: `{"emails":["baz@ory.sh","foo@ory.sh"]}`,
|
||||
schema: "file://./stub/extension/recovery/email.schema.json",
|
||||
expect: []RecoveryAddress{
|
||||
{
|
||||
Value: "foo@ory.sh",
|
||||
Via: AddressTypeEmail,
|
||||
IdentityID: iid,
|
||||
},
|
||||
{
|
||||
Value: "baz@ory.sh",
|
||||
Via: AddressTypeEmail,
|
||||
IdentityID: iid,
|
||||
},
|
||||
},
|
||||
existing: []RecoveryAddress{
|
||||
{
|
||||
Value: "foo@ory.sh",
|
||||
Via: AddressTypeEmail,
|
||||
IdentityID: iid,
|
||||
},
|
||||
{
|
||||
Value: "bar@ory.sh",
|
||||
Via: AddressTypeEmail,
|
||||
IdentityID: iid,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "valid emails, no existing, overlap",
|
||||
doc: `{"emails":["foo@ory.sh","foo@ory.sh","baz@ory.sh"]}`,
|
||||
schema: "file://./stub/extension/recovery/email.schema.json",
|
||||
expect: []RecoveryAddress{
|
||||
{
|
||||
Value: "foo@ory.sh",
|
||||
Via: AddressTypeEmail,
|
||||
IdentityID: iid,
|
||||
},
|
||||
{
|
||||
Value: "baz@ory.sh",
|
||||
Via: AddressTypeEmail,
|
||||
IdentityID: iid,
|
||||
},
|
||||
},
|
||||
existing: []RecoveryAddress{
|
||||
{
|
||||
Value: "foo@ory.sh",
|
||||
Via: AddressTypeEmail,
|
||||
IdentityID: iid,
|
||||
},
|
||||
{
|
||||
Value: "bar@ory.sh",
|
||||
Via: AddressTypeEmail,
|
||||
IdentityID: iid,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "invalid email",
|
||||
doc: `{"emails":["foo@ory.sh","bar@ory.sh"], "username": "foobar"}`,
|
||||
schema: "file://./stub/extension/recovery/email.schema.json",
|
||||
expectErr: errors.New("I[#/username] S[#/properties/username/format] \"foobar\" is not valid \"email\""),
|
||||
},
|
||||
{
|
||||
description: "valid emails, no existing",
|
||||
doc: `{"emails":["foo@ory.sh","bar@ory.sh","bar@ory.sh"], "username": "foobar@ory.sh"}`,
|
||||
schema: "file://./stub/extension/recovery/email.schema.json",
|
||||
expect: []RecoveryAddress{
|
||||
{
|
||||
Value: "foo@ory.sh",
|
||||
Via: AddressTypeEmail,
|
||||
IdentityID: iid,
|
||||
},
|
||||
{
|
||||
Value: "bar@ory.sh",
|
||||
Via: AddressTypeEmail,
|
||||
IdentityID: iid,
|
||||
},
|
||||
{
|
||||
Value: "foobar@ory.sh",
|
||||
Via: AddressTypeEmail,
|
||||
IdentityID: iid,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "valid phone number normalization",
|
||||
doc: `{"telephoneNumber":"+33 6 12 34 56 78"}`,
|
||||
schema: "file://./stub/extension/recovery/sms.schema.json",
|
||||
expect: []RecoveryAddress{
|
||||
{
|
||||
Value: "+33612345678",
|
||||
Via: AddressTypeSMS,
|
||||
IdentityID: iid,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "valid phone number, no existing",
|
||||
doc: `{"telephoneNumber":"+68672098006"}`,
|
||||
schema: "file://./stub/extension/recovery/sms.schema.json",
|
||||
expect: []RecoveryAddress{
|
||||
{
|
||||
Value: "+68672098006",
|
||||
Via: AddressTypeSMS,
|
||||
IdentityID: iid,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "valid phone number, some existing, no overlap",
|
||||
doc: `{"telephoneNumber":"+68672098006"}`,
|
||||
schema: "file://./stub/extension/recovery/sms.schema.json",
|
||||
expect: []RecoveryAddress{
|
||||
{
|
||||
Value: "+68672098006",
|
||||
Via: AddressTypeSMS,
|
||||
IdentityID: iid,
|
||||
},
|
||||
},
|
||||
existing: []RecoveryAddress{
|
||||
{
|
||||
Value: "+12 345 67890123",
|
||||
Via: AddressTypeSMS,
|
||||
IdentityID: iid,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "valid phone number, some existing, overlap",
|
||||
doc: `{"telephoneNumber":"+68672098006"}`,
|
||||
schema: "file://./stub/extension/recovery/sms.schema.json",
|
||||
expect: []RecoveryAddress{
|
||||
{
|
||||
Value: "+68672098006",
|
||||
Via: AddressTypeSMS,
|
||||
IdentityID: iid,
|
||||
},
|
||||
},
|
||||
existing: []RecoveryAddress{
|
||||
{
|
||||
Value: "+68672098006",
|
||||
Via: AddressTypeSMS,
|
||||
IdentityID: iid,
|
||||
},
|
||||
{
|
||||
Value: "+33 07856952",
|
||||
Via: AddressTypeSMS,
|
||||
IdentityID: iid,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "invalid phone number",
|
||||
doc: `{"telephoneNumber": "foobar"}`,
|
||||
schema: "file://./stub/extension/recovery/sms.schema.json",
|
||||
// We get 2 errors: one from the JSON schema `format` validation and one from the normalization error.
|
||||
expectErr: errors.New("I[#/telephoneNumber] S[#/properties/telephoneNumber] validation failed\n I[#/telephoneNumber] S[#/properties/telephoneNumber/format] \"foobar\" is not valid \"tel\"\n I[#/telephoneNumber] S[#/properties/telephoneNumber/format] \"foobar\" is not valid \"tel\" for \"sms\""),
|
||||
},
|
||||
{
|
||||
description: "phone number that fails normalization",
|
||||
doc: `{"telephoneNumber": "+1234"}`, // Too short to be a valid E.164 number
|
||||
schema: "file://./stub/extension/recovery/sms.schema.json",
|
||||
// The JSON schema format validator rejects it, and normalization also fails
|
||||
expectErr: errors.New("I[#/telephoneNumber] S[#/properties/telephoneNumber] validation failed\n I[#/telephoneNumber] S[#/properties/telephoneNumber/format] \"+1234\" is not valid \"tel\"\n I[#/telephoneNumber] S[#/properties/telephoneNumber/format] \"+1234\" is not valid \"tel\" for \"sms\""),
|
||||
},
|
||||
{
|
||||
description: "break_glass_for_organization preserved on existing recovery address",
|
||||
doc: `{"username":"foo@ory.sh"}`,
|
||||
schema: "file://./stub/extension/recovery/email.schema.json",
|
||||
expect: []RecoveryAddress{
|
||||
{
|
||||
Value: "foo@ory.sh",
|
||||
Via: AddressTypeEmail,
|
||||
IdentityID: iid,
|
||||
BreakGlassForOrganization: breakGlassOrgID,
|
||||
},
|
||||
},
|
||||
existing: []RecoveryAddress{
|
||||
{
|
||||
Value: "foo@ory.sh",
|
||||
Via: AddressTypeEmail,
|
||||
IdentityID: iid,
|
||||
BreakGlassForOrganization: breakGlassOrgID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "break_glass null preserved on existing recovery address",
|
||||
doc: `{"username":"foo@ory.sh"}`,
|
||||
schema: "file://./stub/extension/recovery/email.schema.json",
|
||||
expect: []RecoveryAddress{
|
||||
{
|
||||
Value: "foo@ory.sh",
|
||||
Via: AddressTypeEmail,
|
||||
IdentityID: iid,
|
||||
},
|
||||
},
|
||||
existing: []RecoveryAddress{
|
||||
{
|
||||
Value: "foo@ory.sh",
|
||||
Via: AddressTypeEmail,
|
||||
IdentityID: iid,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "docomo email under no-validate is accepted",
|
||||
doc: `{"email":"foo.@docomo.ne.jp"}`,
|
||||
schema: "file://./stub/extension/recovery/no-validate.schema.json",
|
||||
expect: []RecoveryAddress{
|
||||
{
|
||||
Value: "foo.@docomo.ne.jp",
|
||||
Via: AddressTypeEmail,
|
||||
IdentityID: iid,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Uppercase input on the lenient path must still lowercase so it
|
||||
// matches the recovery-address lookup normalization.
|
||||
description: "docomo email under no-validate is normalized",
|
||||
doc: `{"email":"FOO.@docomo.ne.jp"}`,
|
||||
schema: "file://./stub/extension/recovery/no-validate.schema.json",
|
||||
expect: []RecoveryAddress{
|
||||
{
|
||||
Value: "foo.@docomo.ne.jp",
|
||||
Via: AddressTypeEmail,
|
||||
IdentityID: iid,
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("case=%d description=%s", k, tc.description), func(t *testing.T) {
|
||||
id := &Identity{ID: iid, RecoveryAddresses: tc.existing}
|
||||
c := jsonschema.NewCompiler()
|
||||
runner, err := schema.NewExtensionRunner(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
e := NewSchemaExtensionRecovery(id)
|
||||
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
|
||||
}
|
||||
|
||||
require.NoError(t, e.Finish())
|
||||
|
||||
addresses := id.RecoveryAddresses
|
||||
require.Len(t, addresses, len(tc.expect))
|
||||
|
||||
for _, actual := range addresses {
|
||||
var found bool
|
||||
for _, expect := range tc.expect {
|
||||
if reflect.DeepEqual(actual, expect) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "%+v not in %+v", actual, tc.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSchemaExtensionRecovery_PreservesExistingRecordWhenValueIsNotNormalized
|
||||
// is a regression test for the same root cause as #11283's verification bug,
|
||||
// applied to recovery addresses: identities whose persisted
|
||||
// RecoveryAddress.Value is in pre-normalization raw form get their existing
|
||||
// record (ID, BreakGlassForOrganization, timestamps) dropped on every
|
||||
// re-validation because the extension now emits the E.164-normalized form
|
||||
// and has() compares values byte-wise.
|
||||
func TestSchemaExtensionRecovery_PreservesExistingRecordWhenValueIsNotNormalized(t *testing.T) {
|
||||
iid := x.NewUUID()
|
||||
existingID := x.NewUUID()
|
||||
breakGlassOrgID := uuid.NullUUID{UUID: x.NewUUID(), Valid: true}
|
||||
|
||||
rawPhone := "+49 0176 67111638"
|
||||
id := &Identity{
|
||||
ID: iid,
|
||||
RecoveryAddresses: []RecoveryAddress{{
|
||||
ID: existingID,
|
||||
Value: rawPhone,
|
||||
Via: AddressTypeSMS,
|
||||
IdentityID: iid,
|
||||
BreakGlassForOrganization: breakGlassOrgID,
|
||||
}},
|
||||
}
|
||||
|
||||
c := jsonschema.NewCompiler()
|
||||
runner, err := schema.NewExtensionRunner(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
e := NewSchemaExtensionRecovery(id)
|
||||
runner.AddRunner(e).Register(c)
|
||||
|
||||
err = c.MustCompile(t.Context(), "file://./stub/extension/recovery/sms.schema.json").
|
||||
Validate(bytes.NewBufferString(fmt.Sprintf(`{"telephoneNumber":%q}`, rawPhone)))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, e.Finish())
|
||||
|
||||
require.Len(t, id.RecoveryAddresses, 1)
|
||||
assert.Equal(t, existingID, id.RecoveryAddresses[0].ID,
|
||||
"existing recovery record ID must be retained on re-validation with raw phone")
|
||||
assert.Equal(t, breakGlassOrgID, id.RecoveryAddresses[0].BreakGlassForOrganization,
|
||||
"break_glass_for_organization must be retained on re-validation with raw phone")
|
||||
}
|
||||
Reference in New Issue
Block a user