diff --git a/docs/kratos/guides/normalize-phone-numbers.mdx b/docs/kratos/guides/normalize-phone-numbers.mdx new file mode 100644 index 000000000..9bd1ed9f5 --- /dev/null +++ b/docs/kratos/guides/normalize-phone-numbers.mdx @@ -0,0 +1,126 @@ +--- +id: normalize-phone-numbers +title: Normalize phone numbers to E.164 +sidebar_label: Normalize phone numbers +--- + +Ory Kratos normalizes phone numbers to [E.164 format](https://en.wikipedia.org/wiki/E.164) when they're used as identifiers, +verifiable addresses, or recovery addresses. New data is normalized on write. Existing data continues to work through a +backward-compatible lookup, but you should run the `normalize-phone-numbers` migration command after upgrading to converge all +rows to E.164. + +This guide is for self-hosted Kratos administrators (OSS and OEL). Ory Network customers don't need to take any action. + +:::important + +Back up your database before running the migration. The migration doesn't store the original value, therefore there's no automatic +rollback after migration. To revert, you will need to restore your backed-up database. + +::: + +## Why normalize + +Before this change, Kratos stored phone numbers exactly as users entered them. A user who registered with `+49 176 671 11 638` and +another who registered with `+4917667111638` would create two separate identities for the same phone number. Lookups, recovery, +and verification could behave inconsistently depending on the input format. + +After normalization, all phone numbers are stored in E.164 format (for example, `+4917667111638`). Lookups match regardless of how +the user formatted the input. + +## Rollout sequence + +:::caution + +Don't run the migration before deploying the new Kratos version. The previous version does exact-string matching on identifiers. +If you normalize the database first, users who type their phone number in the original (non-E.164) format won't be able to log in +until the new code is deployed. + +::: + +Run the steps in this exact order: + +1. **Deploy the new Kratos version.** + The new code normalizes phone numbers on write and uses a backward-compatible lookup that matches both E.164 and legacy + formats. Existing users can still log in with whatever format they originally registered with. + +2. **Run the migration command.** + After the deploy completes and traffic is stable, run: + + ``` + kratos migrate normalize-phone-numbers + ``` + + Or with the DSN from the environment: + + ``` + export DSN=... + kratos migrate normalize-phone-numbers -e + ``` + + The command iterates over `identity_credential_identifiers`, `identity_verifiable_addresses`, and `identity_recovery_addresses` + and rewrites any non-E.164 phone numbers in place. + +## What the command does + +The command uses keyset pagination to scan three tables in batches: + +| Table | Column | Filter | +| --------------------------------- | ------------ | ---------------------- | +| `identity_credential_identifiers` | `identifier` | `identifier LIKE '+%'` | +| `identity_verifiable_addresses` | `value` | `via = 'sms'` | +| `identity_recovery_addresses` | `value` | `via = 'sms'` | + +For each row, the command parses the value with the [`nyaruka/phonenumbers`](https://github.com/nyaruka/phonenumbers) library and +rewrites it to E.164 if parsing succeeds. Rows that fail to parse (for example, an OIDC subject that happens to start with `+`) +are left untouched and counted as skipped. + +The command is **idempotent**: running it twice is safe. The second run only reports skipped rows. + +## Flags + +| Flag | Default | Description | +| ----------------------- | ------- | ------------------------------------------------------------------------ | +| `-e`, `--read-from-env` | `false` | Read the database connection string from the `DSN` environment variable. | +| `-b`, `--batch-size` | `1000` | Number of rows to process per batch. | +| `--dry-run` | `false` | Report what would change without writing. | + +Use `--dry-run` first to preview the changes: + +``` +kratos migrate normalize-phone-numbers --dry-run -e +``` + +Each row that would be updated is printed in the form: + +``` +[dry-run] identity_credential_identifiers : "+49 176 671 11 638" -> "+4917667111638" +``` + +## Output + +After processing all three tables, the command prints a summary: + +``` +=== Summary === +identity_credential_identifiers: scanned=1234 updated=42 skipped=1192 errors=0 +identity_verifiable_addresses: scanned=987 updated=15 skipped=972 errors=0 +identity_recovery_addresses: scanned=987 updated=15 skipped=972 errors=0 +``` + +- `scanned`: rows examined. +- `updated`: rows rewritten to E.164 (or rows that _would_ be rewritten in dry-run mode). +- `skipped`: rows already in E.164 format, or values that aren't valid phone numbers. +- `errors`: rows that failed to update. Errors are logged to stderr with the row ID and source value. + +## Duplicate handling + +If the migration finds two rows that normalize to the same E.164 value (for example, `+49 176 671 11 638` and `+4917667111638` for +the same user), the update fails on the second row with a unique constraint violation, which the command logs as an error and +skips. You can resolve the duplicate manually and re-run the command. + +In practice, duplicates are rare. Most identities have only one phone identifier per credential type. + +## Rolling back + +The migration only converts non-E.164 values to E.164. It doesn't store the original value, so there's no automatic rollback. If +you need to revert, restore from the backup you took before running the command. diff --git a/docs/kratos/guides/upgrade.mdx b/docs/kratos/guides/upgrade.mdx index feb7a7e35..5ca412d93 100644 --- a/docs/kratos/guides/upgrade.mdx +++ b/docs/kratos/guides/upgrade.mdx @@ -17,6 +17,9 @@ Back up your data! Applying upgrades can lead to data loss if handled incorrectl 1. Update the [Ory Kratos SDK](../sdk/01_overview.md) if used in your application. 1. [Install](../install.mdx) the new version of Ory Kratos. 1. Run [`kratos migrate sql`](../cli/kratos-migrate-sql.md) to run the SQL migrations to the new database schema. +1. If you are upgrading to a version that introduces phone number normalization, run `kratos migrate normalize-phone-numbers` + after the new version is deployed and serving traffic. This rewrites existing phone identifiers to E.164 format. See the + [phone normalization guide](./normalize-phone-numbers.mdx) for the recommended rollout sequence. Should you run into problems with the upgrade, consider a stepped upgrade and please visit the community [chat](https://slack.ory.com/) or start a [discussion](https://github.com/ory/kratos/discussions). diff --git a/sidebars-oel.ts b/sidebars-oel.ts index 165200fbc..61e2d7491 100644 --- a/sidebars-oel.ts +++ b/sidebars-oel.ts @@ -72,6 +72,7 @@ const oelSidebar = [ "kratos/guides/https-tls", "kratos/guides/hosting-own-have-i-been-pwned-api", "kratos/guides/secret-key-rotation", + "kratos/guides/normalize-phone-numbers", { type: "category", label: "Troubleshooting", diff --git a/sidebars-oss.ts b/sidebars-oss.ts index b78956292..0cc2fef62 100644 --- a/sidebars-oss.ts +++ b/sidebars-oss.ts @@ -1,12 +1,3 @@ -// sidebars-oss.ts - -import { - SidebarItem, - SidebarItemConfig, -} from "@docusaurus/plugin-content-docs/src/sidebars/types" - -type SidebarItemsConfig = SidebarItemConfig[] - const ossSidebar = [ { type: "category", @@ -88,6 +79,7 @@ const ossSidebar = [ "kratos/guides/https-tls", "kratos/guides/hosting-own-have-i-been-pwned-api", "kratos/guides/secret-key-rotation", + "kratos/guides/normalize-phone-numbers", { type: "category", label: "Troubleshooting",