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:
336
identity/extension_credentials_test.go
Normal file
336
identity/extension_credentials_test.go
Normal file
@@ -0,0 +1,336 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package identity_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"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/identity"
|
||||
"github.com/ory/kratos/schema"
|
||||
"github.com/ory/kratos/x"
|
||||
"github.com/ory/x/snapshotx"
|
||||
"github.com/ory/x/sqlxx"
|
||||
)
|
||||
|
||||
func TestSchemaExtensionCredentials(t *testing.T) {
|
||||
for k, tc := range []struct {
|
||||
expectErr error
|
||||
schema string
|
||||
doc string
|
||||
expectedIdentifiers []string
|
||||
existing *identity.Credentials
|
||||
ct identity.CredentialsType
|
||||
}{
|
||||
{
|
||||
doc: `{"email":"foo@ory.sh"}`,
|
||||
schema: "file://./stub/extension/credentials/schema.json",
|
||||
expectedIdentifiers: []string{"foo@ory.sh"},
|
||||
ct: identity.CredentialsTypePassword,
|
||||
},
|
||||
{
|
||||
doc: `{"emails":["foo😀@ory.sh","foo😀@ory.sh","bar@ory.sh"], "username": "Foo🥳🖐baR"}`,
|
||||
schema: "file://./stub/extension/credentials/multi.schema.json",
|
||||
expectedIdentifiers: []string{"foo😀@ory.sh", "bar@ory.sh", "foo🥳🖐bar"},
|
||||
ct: identity.CredentialsTypePassword,
|
||||
},
|
||||
{
|
||||
doc: `{"emails":["föo@ory.sh","föo@ory.sh","bar@ory.sh"], "username": "foobar"}`,
|
||||
schema: "file://./stub/extension/credentials/multi.schema.json",
|
||||
expectedIdentifiers: []string{"föo@ory.sh", "bar@ory.sh"},
|
||||
ct: identity.CredentialsTypeWebAuthn,
|
||||
},
|
||||
{
|
||||
doc: `{"emails":["FÖÖ@ory.sh","bar@ory.sh"], "username": "foobär"}`,
|
||||
schema: "file://./stub/extension/credentials/multi.schema.json",
|
||||
expectedIdentifiers: []string{"föö@ory.sh", "bar@ory.sh", "foobär"},
|
||||
existing: &identity.Credentials{
|
||||
Identifiers: []string{"not-foo@ory.sh"},
|
||||
},
|
||||
ct: identity.CredentialsTypePassword,
|
||||
},
|
||||
{
|
||||
doc: `{"email":"foo@ory.sh"}`,
|
||||
schema: "file://./stub/extension/credentials/webauthn.schema.json",
|
||||
expectedIdentifiers: []string{"foo@ory.sh"},
|
||||
ct: identity.CredentialsTypeWebAuthn,
|
||||
},
|
||||
{
|
||||
doc: `{"email":"FOO@ory.sh"}`,
|
||||
schema: "file://./stub/extension/credentials/webauthn.schema.json",
|
||||
expectedIdentifiers: []string{"foo@ory.sh"},
|
||||
existing: &identity.Credentials{
|
||||
Identifiers: []string{"not-foo@ory.sh"},
|
||||
},
|
||||
ct: identity.CredentialsTypeWebAuthn,
|
||||
},
|
||||
{
|
||||
doc: `{"email":"foo@ory.sh"}`,
|
||||
schema: "file://./stub/extension/credentials/code.schema.json",
|
||||
expectedIdentifiers: []string{"foo@ory.sh"},
|
||||
ct: identity.CredentialsTypeCodeAuth,
|
||||
},
|
||||
{
|
||||
doc: `{"email":"FOO@ory.sh"}`,
|
||||
schema: "file://./stub/extension/credentials/code.schema.json",
|
||||
expectedIdentifiers: []string{"foo@ory.sh"},
|
||||
existing: &identity.Credentials{
|
||||
Identifiers: []string{"not-foo@ory.sh"},
|
||||
},
|
||||
ct: identity.CredentialsTypeCodeAuth,
|
||||
},
|
||||
{
|
||||
doc: `{"email":"FOO@ory.sh"}`,
|
||||
schema: "file://./stub/extension/credentials/code.schema.json",
|
||||
expectedIdentifiers: []string{"foo@ory.sh"},
|
||||
existing: &identity.Credentials{
|
||||
Identifiers: []string{"not-foo@ory.sh", "foo@ory.sh"},
|
||||
Config: sqlxx.JSONRawMessage(`{"addresses":[{"channel":"email","address":"not-foo@ory.sh"}]}`),
|
||||
},
|
||||
ct: identity.CredentialsTypeCodeAuth,
|
||||
},
|
||||
{
|
||||
doc: `{"email":"FOO@ory.sh","phone":"+49 176 671 11 638"}`,
|
||||
schema: "file://./stub/extension/credentials/code-phone-email.schema.json",
|
||||
expectedIdentifiers: []string{"+4917667111638", "foo@ory.sh"},
|
||||
existing: &identity.Credentials{
|
||||
Identifiers: []string{"not-foo@ory.sh", "foo@ory.sh"},
|
||||
Config: sqlxx.JSONRawMessage(`{"addresses":[{"channel":"email","address":"not-foo@ory.sh"}]}`),
|
||||
},
|
||||
ct: identity.CredentialsTypeCodeAuth,
|
||||
},
|
||||
{
|
||||
doc: `{"email":"FOO@ory.sh","phone":"+49 176 671 11 638"}`,
|
||||
schema: "file://./stub/extension/credentials/code-phone-email.schema.json",
|
||||
expectedIdentifiers: []string{"+4917667111638", "foo@ory.sh"},
|
||||
existing: &identity.Credentials{
|
||||
Identifiers: []string{"not-foo@ory.sh", "foo@ory.sh"},
|
||||
Config: sqlxx.JSONRawMessage(`{"addresses":[{"channel":"email","address":"not-foo@ory.sh"}]}`),
|
||||
},
|
||||
ct: identity.CredentialsTypeCodeAuth,
|
||||
},
|
||||
{
|
||||
doc: `{"email":"FOO@ory.sh","email2":"FOO@ory.sh","phone":"+49 176 671 11 638"}`,
|
||||
schema: "file://./stub/extension/credentials/code-phone-email.schema.json",
|
||||
expectedIdentifiers: []string{"+4917667111638", "foo@ory.sh"},
|
||||
existing: &identity.Credentials{
|
||||
Identifiers: []string{"not-foo@ory.sh", "fOo@ory.sh"},
|
||||
Config: sqlxx.JSONRawMessage(`{"addresses":[{"channel":"email","address":"not-foo@ory.sh"}]}`),
|
||||
},
|
||||
ct: identity.CredentialsTypeCodeAuth,
|
||||
},
|
||||
{
|
||||
doc: `{"email":"FOO@ory.sh","email2":"FOO@ory.sh","email3":"bar@ory.sh","phone":"+49 176 671 11 638"}`,
|
||||
schema: "file://./stub/extension/credentials/code-phone-email.schema.json",
|
||||
expectedIdentifiers: []string{"+4917667111638", "foo@ory.sh", "bar@ory.sh"},
|
||||
existing: &identity.Credentials{
|
||||
Identifiers: []string{"not-foo@ory.sh", "fOo@ory.sh"},
|
||||
Config: sqlxx.JSONRawMessage(`{"addresses":[{"channel":"email","address":"not-foo@ory.sh"}]}`),
|
||||
},
|
||||
ct: identity.CredentialsTypeCodeAuth,
|
||||
},
|
||||
{
|
||||
doc: `{"email":"bar@ory.sh"}`,
|
||||
schema: "file://./stub/extension/credentials/email.schema.json",
|
||||
expectedIdentifiers: []string{"bar@ory.sh"},
|
||||
existing: &identity.Credentials{
|
||||
Identifiers: []string{"foo@ory.sh"},
|
||||
Config: sqlxx.JSONRawMessage(`{"addresses":[{"channel":"email","address":"foo@ory.sh"}]}`),
|
||||
},
|
||||
ct: identity.CredentialsTypeCodeAuth,
|
||||
},
|
||||
|
||||
{
|
||||
doc: `{"email":"foo@ory.sh","email2":"foo@ory.sh","email3":"bar@ory.sh","phone":"+49 176 671 11 638"}`,
|
||||
schema: "file://./stub/extension/credentials/code-phone-email.schema.json",
|
||||
expectedIdentifiers: []string{"foo@ory.sh", "bar@ory.sh", "+4917667111638"},
|
||||
ct: identity.CredentialsTypePassword,
|
||||
},
|
||||
{
|
||||
doc: `{"email":"foo@ory.sh","phone":"+49 176 671 11 638"}`,
|
||||
schema: "file://./stub/extension/credentials/code-phone-email.schema.json",
|
||||
expectedIdentifiers: []string{"foo@ory.sh", "+4917667111638"},
|
||||
existing: &identity.Credentials{
|
||||
Identifiers: []string{"foo@ory.sh", "+49 176 671 11 638"},
|
||||
},
|
||||
ct: identity.CredentialsTypePassword,
|
||||
},
|
||||
{
|
||||
doc: `{"emails":["bar@o😶ry.sh"], "username": "Foo🥳🖐baR"}`,
|
||||
schema: "file://./stub/extension/credentials/multi.schema.json",
|
||||
ct: identity.CredentialsTypePassword,
|
||||
expectErr: errors.New("I[#/emails/0] S[#/properties/emails/items/format] \"bar@o😶ry.sh\" is not valid \"email\""),
|
||||
},
|
||||
{
|
||||
doc: `{"email":"foo@öry.sh"}`,
|
||||
schema: "file://./stub/extension/credentials/webauthn.schema.json",
|
||||
expectErr: errors.New(`I[#/email] S[#/properties/email/format] "foo@öry.sh" is not valid "email"`),
|
||||
ct: identity.CredentialsTypeWebAuthn,
|
||||
},
|
||||
|
||||
// Invisible and bidi-control characters must be stripped from both
|
||||
// emails and usernames. Each injected rune below has been used in
|
||||
// real homograph / duplicate-account bypasses: the poisoned input
|
||||
// must canonicalize to the same baseline an attacker is trying to
|
||||
// impersonate, so the UNIQUE constraint on
|
||||
// identity_credential_identifiers rejects the duplicate.
|
||||
//
|
||||
// The runes are built with fmt.Sprintf("%c", …) on purpose: literal
|
||||
// invisible characters in test source are unreviewable, and a literal
|
||||
// U+FEFF (BOM) is rejected by the Go compiler outright.
|
||||
|
||||
// Zero-width space (U+200B) inside an email local part.
|
||||
{
|
||||
doc: fmt.Sprintf(`{"emails":["ad%cmin@ory.sh"],"username":"zwsp-email"}`, 0x200B),
|
||||
schema: "file://./stub/extension/credentials/multi.schema.json",
|
||||
expectedIdentifiers: []string{"admin@ory.sh", "zwsp-email"},
|
||||
ct: identity.CredentialsTypePassword,
|
||||
},
|
||||
// Several different invisibles (ZWSP, ZWJ, soft hyphen) in one email.
|
||||
{
|
||||
doc: fmt.Sprintf(`{"emails":["a%cd%cmi%cn@ory.sh"],"username":"multi-email"}`, 0x200B, 0x200D, 0x00AD),
|
||||
schema: "file://./stub/extension/credentials/multi.schema.json",
|
||||
expectedIdentifiers: []string{"admin@ory.sh", "multi-email"},
|
||||
ct: identity.CredentialsTypePassword,
|
||||
},
|
||||
// BOM (U+FEFF) prefix and word joiner (U+2060) suffix in an email.
|
||||
{
|
||||
doc: fmt.Sprintf(`{"emails":["%cadmin%c@ory.sh"],"username":"bom-email"}`, 0xFEFF, 0x2060),
|
||||
schema: "file://./stub/extension/credentials/multi.schema.json",
|
||||
expectedIdentifiers: []string{"admin@ory.sh", "bom-email"},
|
||||
ct: identity.CredentialsTypePassword,
|
||||
},
|
||||
// Right-to-left override (U+202E, Trojan-source style) in an email.
|
||||
{
|
||||
doc: fmt.Sprintf(`{"emails":["ad%cmin@ory.sh"],"username":"rlo-email"}`, 0x202E),
|
||||
schema: "file://./stub/extension/credentials/multi.schema.json",
|
||||
expectedIdentifiers: []string{"admin@ory.sh", "rlo-email"},
|
||||
ct: identity.CredentialsTypePassword,
|
||||
},
|
||||
// Zero-width space (U+200B) inside a username.
|
||||
{
|
||||
doc: fmt.Sprintf(`{"emails":["zwsp-user@ory.sh"],"username":"ad%cmin"}`, 0x200B),
|
||||
schema: "file://./stub/extension/credentials/multi.schema.json",
|
||||
expectedIdentifiers: []string{"zwsp-user@ory.sh", "admin"},
|
||||
ct: identity.CredentialsTypePassword,
|
||||
},
|
||||
// Default-ignorable runes outside category Cf in a username: ZWNJ,
|
||||
// combining grapheme joiner (U+034F), Hangul filler (U+3164), and
|
||||
// variation selector-16 (U+FE0F) all collapse to the baseline.
|
||||
{
|
||||
doc: fmt.Sprintf(`{"emails":["multi-user@ory.sh"],"username":"a%cd%cm%ci%cn"}`, 0x200C, 0x034F, 0x3164, 0xFE0F),
|
||||
schema: "file://./stub/extension/credentials/multi.schema.json",
|
||||
expectedIdentifiers: []string{"multi-user@ory.sh", "admin"},
|
||||
ct: identity.CredentialsTypePassword,
|
||||
},
|
||||
// Bidi override (U+202E) plus word joiner (U+2060) and soft hyphen
|
||||
// (U+00AD) wrapping a username.
|
||||
{
|
||||
doc: fmt.Sprintf(`{"emails":["rlo-user@ory.sh"],"username":"%cad%cmin%c"}`, 0x202E, 0x2060, 0x00AD),
|
||||
schema: "file://./stub/extension/credentials/multi.schema.json",
|
||||
expectedIdentifiers: []string{"rlo-user@ory.sh", "admin"},
|
||||
ct: identity.CredentialsTypePassword,
|
||||
},
|
||||
// Two differently-poisoned variants of the same email must dedupe to
|
||||
// a single identifier — this is the duplicate-account bypass itself.
|
||||
{
|
||||
doc: fmt.Sprintf(`{"emails":["ad%cmin@ory.sh","ad%cmin@ory.sh"],"username":"dedup"}`, 0x200B, 0x00AD),
|
||||
schema: "file://./stub/extension/credentials/multi.schema.json",
|
||||
expectedIdentifiers: []string{"admin@ory.sh", "dedup"},
|
||||
ct: identity.CredentialsTypePassword,
|
||||
},
|
||||
// Boundary: NFKC does not fold script confusables. A Cyrillic "а"
|
||||
// (U+0430) is a distinct letter from Latin "a", so this username
|
||||
// stays distinct. Confusable-script detection is intentionally out
|
||||
// of scope for identifier normalization.
|
||||
{
|
||||
doc: fmt.Sprintf(`{"emails":["homoglyph@ory.sh"],"username":"%cdmin"}`, 0x0430),
|
||||
schema: "file://./stub/extension/credentials/multi.schema.json",
|
||||
expectedIdentifiers: []string{"homoglyph@ory.sh", fmt.Sprintf("%cdmin", 0x0430)},
|
||||
ct: identity.CredentialsTypePassword,
|
||||
},
|
||||
// Two emails that differ only by a confusable character — Latin "a"
|
||||
// versus Cyrillic "а" (U+0430) — are not folded by NFKC, so both are
|
||||
// taken as distinct identifiers rather than collapsing into one.
|
||||
{
|
||||
doc: fmt.Sprintf(`{"emails":["admin@ory.sh","%cdmin@ory.sh"],"username":"confusable-emails"}`, 0x0430),
|
||||
schema: "file://./stub/extension/credentials/multi.schema.json",
|
||||
expectedIdentifiers: []string{"admin@ory.sh", "аdmin@ory.sh", "confusable-emails"},
|
||||
ct: identity.CredentialsTypePassword,
|
||||
},
|
||||
{
|
||||
doc: `{"email":"foo.@docomo.ne.jp"}`,
|
||||
schema: "file://./stub/extension/credentials/code-no-validate.schema.json",
|
||||
expectedIdentifiers: []string{"foo.@docomo.ne.jp"},
|
||||
ct: identity.CredentialsTypeCodeAuth,
|
||||
},
|
||||
// Uppercase input under no-validate must normalize to match the
|
||||
// login-code lookup, which compares x.GracefulNormalization(identifier).
|
||||
{
|
||||
doc: `{"email":"FOO.@docomo.ne.jp"}`,
|
||||
schema: "file://./stub/extension/credentials/code-no-validate.schema.json",
|
||||
expectedIdentifiers: []string{"foo.@docomo.ne.jp"},
|
||||
ct: identity.CredentialsTypeCodeAuth,
|
||||
},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
|
||||
c := jsonschema.NewCompiler()
|
||||
runner, err := schema.NewExtensionRunner(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
i := new(identity.Identity)
|
||||
e := identity.NewSchemaExtensionCredentials(i)
|
||||
if tc.existing != nil {
|
||||
i.SetCredentials(tc.ct, *tc.existing)
|
||||
}
|
||||
|
||||
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, err)
|
||||
require.NoError(t, e.Finish())
|
||||
|
||||
credentials, ok := i.GetCredentials(tc.ct)
|
||||
require.True(t, ok)
|
||||
assert.ElementsMatch(t, tc.expectedIdentifiers, credentials.Identifiers)
|
||||
snapshotx.SnapshotT(t, credentials, snapshotx.ExceptPaths("identifiers"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchemaExtensionCredentialsCodeNoValidateAddress(t *testing.T) {
|
||||
c := jsonschema.NewCompiler()
|
||||
runner, err := schema.NewExtensionRunner(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
i := new(identity.Identity)
|
||||
e := identity.NewSchemaExtensionCredentials(i)
|
||||
runner.AddRunner(e).Register(c)
|
||||
|
||||
require.NoError(t, c.MustCompile(t.Context(), "file://./stub/extension/credentials/code-no-validate.schema.json").
|
||||
Validate(bytes.NewBufferString(`{"email":"FOO.@docomo.ne.jp"}`)))
|
||||
require.NoError(t, e.Finish())
|
||||
|
||||
cred, ok := i.GetCredentials(identity.CredentialsTypeCodeAuth)
|
||||
require.True(t, ok)
|
||||
|
||||
var conf identity.CredentialsCode
|
||||
require.NoError(t, json.Unmarshal(cred.Config, &conf))
|
||||
require.Len(t, conf.Addresses, 1)
|
||||
// The stored address must equal what login-code normalization produces,
|
||||
// or loginSendCode can never match it (strategy_login.go:521).
|
||||
assert.Equal(t, x.GracefulNormalization("FOO.@docomo.ne.jp"), conf.Addresses[0].Address)
|
||||
assert.Equal(t, identity.CodeChannelEmail, conf.Addresses[0].Channel)
|
||||
}
|
||||
Reference in New Issue
Block a user