Skip to content

Username and email are not NFC normalized #4698

Description

@FranjoMindek

Problem

normalizeProviderUserId folds email and username identifiers with .toLowerCase() and nothing else.
It does not normalize Unicode, so text that is visually identical but differently encoded is treated as two distinct identifiers.

ü has two encodings: U+00FC precomposed, and u + U+0308 (combining diaeresis). macOS and some IME and paste paths emit the decomposed form, so which one a user produces depends on their device, not on what they typed.

const precomposed = "jürgen@münchen.de";
const decomposed = precomposed.normalize("NFD");

precomposed === decomposed;                             // false
precomposed.toLowerCase() === decomposed.toLowerCase(); // false  <- what we do today

Consequence: the same person signing up on two devices can end up with two accounts that render identically everywhere in the UI, and neither the unique constraint on providerUserId nor any error message will indicate anything is wrong.

RFC 6532 section 3.1 recommends NFC for exactly this reason.

Scope

Both branches of the email / username case in normalizeProviderUserId are affected, and username is the more exposed of the two:

So decomposed identifiers already exist in production databases

Why this is not a one-line fix

Adding .normalize("NFC") to normalizeProviderUserId would lock out every existing user whose stored identifier is decomposed, silently and unrecoverably.

Every lookup path goes through createProviderId, so normalizing on read without migrating the stored data means the row stops matching:

  • login.ts normalizes the submitted identifier, misses the stored row, returns "Invalid credentials"
  • requestPasswordReset.ts performs the same lookup, so password reset misses too, and fails silently by design
  • The row is still in the database, just unreachable

Breaking change concern

This is a breaking change for affected deployments.
It should explain how to backfill existing AuthIdentity.providerUserId rows for the email and username providers.
It should also warn that collisions can exist: if two existing rows normalize to the same value, they are duplicate accounts that the migration has to reconcile.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions