Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions docs/kratos/guides/normalize-phone-numbers.mdx
Original file line number Diff line number Diff line change
@@ -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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This guide is for self-hosted Kratos administrators (OSS and OEL). Ory Network customers don't need to take any action.
This guide is for self-hosted Ory 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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Before this change, Kratos stored phone numbers exactly as users entered them. A user who registered with `+49 176 671 11 638` and
Before this change, Ory 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.**
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
1. **Deploy the new Kratos version.**
1. Deploy the new Ory Kratos version.

Please check the styleguide in README.md or give it to your LLM - no bolding.

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.**
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
2. **Run the migration command.**
2. Run the migration command.

See above

After the deploy completes and traffic is stable, run:

```
kratos migrate normalize-phone-numbers <database-url>
```

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 `+`)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
rewrites it to E.164 if parsing succeeds. Rows that fail to parse (for example, an OIDC subject that happens to start with `+`)
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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The command is **idempotent**: running it twice is safe. The second run only reports skipped rows.
The command is idempotent: running it twice is safe. The second run only reports skipped rows.

see above


## 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 <id>: "+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.
3 changes: 3 additions & 0 deletions docs/kratos/guides/upgrade.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
1 change: 1 addition & 0 deletions sidebars-oel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 1 addition & 9 deletions sidebars-oss.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{
type: "category",
label: "Troubleshooting",
Expand Down
Loading