diff --git a/docs/kratos/guides/normalize-phone-numbers.mdx b/docs/kratos/guides/normalize-phone-numbers.mdx new file mode 100644 index 000000000..58405c6d4 --- /dev/null +++ b/docs/kratos/guides/normalize-phone-numbers.mdx @@ -0,0 +1,125 @@ +--- +id: normalize-phone-numbers +title: Normalize phone numbers to E.164 +sidebar_label: Normalize phone numbers +--- + +Starting with this release, 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. + +:::info + +Back up your database before running the migration. + +::: + +## 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 + +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. + +:::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. + +::: + +## 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 d8171bea6..ae5a351ed 100644 --- a/docs/kratos/guides/upgrade.mdx +++ b/docs/kratos/guides/upgrade.mdx @@ -11,13 +11,15 @@ Back up your data! Applying upgrades can lead to data loss if handled incorrectl ::: -1. Review breaking changes. - Visit the [CHANGELOG.md](https://github.com/ory/kratos/blob/master/CHANGELOG.md) to see if breaking changes have been - introduced in the version you are upgrading to. +1. Review breaking changes. Visit the [CHANGELOG.md](https://github.com/ory/kratos/blob/master/CHANGELOG.md) to see if breaking + changes have been introduced in the version you are upgrading to. 1. Backup your data. 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). @@ -26,5 +28,5 @@ Should you run into problems with the upgrade, consider a stepped upgrade and pl In [Ory Kratos v0.7](https://github.com/ory/kratos/blob/v0.7.0-alpha.1/CHANGELOG.md#breaking-changes) the cookie behavior has changed. Review -[changes in the exemplary self-service user interface](https://github.com/ory/kratos-selfservice-ui-node/commit/e7fa292968111e06401fcfc9b1dd0e8e285a4d87). +[changes in the exemplary self-service user interface](https://github.com/ory/kratos-selfservice-ui-node/commit/e7fa292968111e06401fcfc9b1dd0e8e285a4d87). Visit the [Cookie Settings](https://www.ory.com/kratos/docs/guides/multi-domain-cookies/#cookies) document for more information. diff --git a/package-lock.json b/package-lock.json index e1720c126..1ba744161 100644 --- a/package-lock.json +++ b/package-lock.json @@ -271,6 +271,7 @@ "version": "5.44.0", "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.44.0.tgz", "integrity": "sha512-/FRKUM1G4xn3vV8+9xH1WJ9XknU8rkBGlefruq9jDhYUAvYozKimhrmC2pRqw/RyHhPivmgZCRuC8jHP8piz4Q==", + "peer": true, "dependencies": { "@algolia/client-common": "5.44.0", "@algolia/requester-browser-xhr": "5.44.0", @@ -447,6 +448,7 @@ "node_modules/@babel/core": { "version": "7.26.0", "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.0", @@ -2433,6 +2435,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": ">=18" }, @@ -2454,6 +2457,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": ">=18" } @@ -2558,6 +2562,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -2963,6 +2968,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -3901,6 +3907,7 @@ "version": "3.9.2", "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz", "integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==", + "peer": true, "dependencies": { "@docusaurus/core": "3.9.2", "@docusaurus/logger": "3.9.2", @@ -4185,6 +4192,7 @@ "version": "3.9.2", "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.9.2.tgz", "integrity": "sha512-6c4DAbR6n6nPbnZhY2V3tzpnKnGL+6aOsLvFL26VRqhlczli9eWG0VDUNoCQEPnGwDMhPS42UhSAnz5pThm5Ag==", + "peer": true, "dependencies": { "@docusaurus/mdx-loader": "3.9.2", "@docusaurus/module-type-aliases": "3.9.2", @@ -4288,6 +4296,7 @@ "version": "3.9.2", "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.9.2.tgz", "integrity": "sha512-lBSBiRruFurFKXr5Hbsl2thmGweAPmddhF3jb99U4EMDA5L+e5Y1rAkOS07Nvrup7HUMBDrCV45meaxZnt28nQ==", + "peer": true, "dependencies": { "@docusaurus/logger": "3.9.2", "@docusaurus/types": "3.9.2", @@ -5470,6 +5479,7 @@ "node_modules/@mdx-js/react": { "version": "3.1.0", "license": "MIT", + "peer": true, "dependencies": { "@types/mdx": "^2.0.0" }, @@ -5529,6 +5539,7 @@ "node_modules/@octokit/core": { "version": "5.2.0", "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -5877,6 +5888,7 @@ "node_modules/@rjsf/core": { "version": "5.24.1", "license": "Apache-2.0", + "peer": true, "dependencies": { "lodash": "^4.17.21", "lodash-es": "^4.17.21", @@ -5897,6 +5909,7 @@ "resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.24.1.tgz", "integrity": "sha512-A25fFj/TNz5bKikCIs20DiedKAalLuAQ7vUX9VQkD2hps5C9YVr0dJgSlsPa5kzl6lQMaRsNouTx8E1ZdLV2fg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "json-schema-merge-allof": "^0.8.1", "jsonpointer": "^5.0.1", @@ -6186,6 +6199,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -6285,6 +6299,7 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@swc/counter": "^0.1.2", "@swc/types": "^0.1.5" @@ -7261,7 +7276,8 @@ }, "node_modules/@types/json-schema": { "version": "7.0.15", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/keyv": { "version": "3.1.4", @@ -7293,6 +7309,7 @@ "node_modules/@types/node": { "version": "22.8.4", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.19.8" } @@ -7335,6 +7352,7 @@ "node_modules/@types/react": { "version": "18.3.12", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -7657,6 +7675,7 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7737,6 +7756,7 @@ "node_modules/ajv": { "version": "8.17.1", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -7778,6 +7798,7 @@ "version": "5.44.0", "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.44.0.tgz", "integrity": "sha512-f8IpsbdQjzTjr/4mJ/jv5UplrtyMnnciGax6/B0OnLCs2/GJTK13O4Y7Ff1AvJVAaztanH+m5nzPoUq6EAy+aA==", + "peer": true, "dependencies": { "@algolia/abtesting": "1.10.0", "@algolia/client-abtesting": "5.44.0", @@ -8070,6 +8091,7 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -8566,6 +8588,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -8929,6 +8952,7 @@ "version": "11.0.3", "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", @@ -9570,6 +9594,7 @@ "version": "3.38.1", "hasInstallScript": true, "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" @@ -9859,6 +9884,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -10169,6 +10195,7 @@ "node_modules/cytoscape": { "version": "3.31.0", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10" } @@ -10534,6 +10561,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "peer": true, "engines": { "node": ">=12" } @@ -12070,6 +12098,7 @@ "node_modules/file-loader/node_modules/ajv": { "version": "6.12.6", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -14731,6 +14760,7 @@ "node_modules/jsep": { "version": "1.4.0", "license": "MIT", + "peer": true, "engines": { "node": ">= 10.16.0" } @@ -17892,6 +17922,7 @@ "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.13.7.tgz", "integrity": "sha512-aChaVU/DO5aRPmk1GX8L+whocagUUpBQqoPtJk+cm7UOXUk87J4PeWCh6nNmTTIfEhiR9DI/+FnA8dln/hTK7g==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/mobx" @@ -18372,6 +18403,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -19352,6 +19384,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", @@ -20207,6 +20240,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -20775,6 +20809,7 @@ "version": "3.2.5", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -21166,6 +21201,7 @@ "version": "6.12.6", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -21232,6 +21268,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.0.1.tgz", "integrity": "sha512-nVRaZCuEyvu69sWrkdwjP6QY57C+lY+uMNNMyWUFJb9Z/JlaBOQus7mSMfGYsblv7R691u6SSJA/dX9IRnyyLQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -21241,6 +21278,7 @@ "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.6.8.tgz", "integrity": "sha512-yD6uN78XlFOkETQp6GRuVe0s5509x3XYx8PfPbirwFTYCj5/RfmSs9YZGCwkUrhZNFzj7tZPdpb+3k50mK1E4g==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.14.0", "@restart/context": "^2.1.4", @@ -21270,6 +21308,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.1.tgz", "integrity": "sha512-3TJg51HSbJiLVYCS6vWwWsyqoS36aGEOCmtLLHxROlSZZ5Bk10xpxHFbrCu4DdqgR85DDc9Vucxqhai3g2xjtA==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.25.0" }, @@ -21322,6 +21361,7 @@ "name": "@docusaurus/react-loadable", "version": "6.0.0", "license": "MIT", + "peer": true, "dependencies": { "@types/react": "*" }, @@ -21366,6 +21406,7 @@ "node_modules/react-router": { "version": "5.3.4", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -23816,6 +23857,7 @@ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.18.tgz", "integrity": "sha512-Mvf3gJFzZCkhjY2Y/Fx9z1m3dxbza0uI9H1CbNZm/jSHCojzJhQ0R7bByrlFJINnMzz/gPulpoFFGymNwrsMcw==", "license": "MIT", + "peer": true, "dependencies": { "@emotion/is-prop-valid": "1.2.2", "@emotion/unitless": "0.8.1", @@ -24100,6 +24142,7 @@ "node_modules/terser-webpack-plugin/node_modules/ajv": { "version": "6.12.6", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -24528,6 +24571,7 @@ "version": "10.9.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -24573,7 +24617,8 @@ }, "node_modules/tslib": { "version": "2.8.0", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tty-browserify": { "version": "0.0.1", @@ -25054,6 +25099,7 @@ "node_modules/url-loader/node_modules/ajv": { "version": "6.12.6", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -25386,6 +25432,7 @@ "node_modules/webpack": { "version": "5.95.0", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", @@ -25666,6 +25713,7 @@ "node_modules/webpack/node_modules/ajv": { "version": "6.12.6", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -26189,6 +26237,7 @@ "version": "4.1.12", "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" }