<a id="identifier-migration"></a>

<a id="id1"></a>

# Identifier migration

Authentication looks up the current `(provider, id_key, uid)` first. If it
does not exist, it looks up previous keys and their values from the authenticated
provider response. These queries use the existing provider/UID index; they do
not search every association’s JSON data. Empty-key historical rows are also
considered using the same indexed UID lookups.

An old email or username locates a candidate, but does not prove that the
candidate belongs to the current provider account. The candidate’s saved current
identifier must match the authenticated identifier. Multiple candidates,
conflicting evidence, and invalid evidence stop authentication. Missing evidence
also stops authentication unless the compatibility policy below permits it.
Migration updates both identifier fields atomically and revalidates evidence
under the association lock.

<a id="configuration-changes"></a>

## Configuration changes

Before switching identifiers, configure extra data to save the proposed new
stable field while the old key is still active. Existing users must authenticate
in this configuration, or receive an independently verified administrative
backfill, before their associations contain that evidence. Adding an extra-data
setting does not populate historical rows automatically.

For example, while Google still uses email:

```default
SOCIAL_AUTH_GOOGLE_OAUTH2_ID_KEY = "email"
SOCIAL_AUTH_GOOGLE_OAUTH2_EXTRA_DATA = [("sub", "sub")]
```

Then switch the active key and retain the historical key:

```default
SOCIAL_AUTH_GOOGLE_OAUTH2_ID_KEY = "sub"
SOCIAL_AUTH_GOOGLE_OAUTH2_LEGACY_ID_KEYS = ["email"]
SOCIAL_AUTH_GOOGLE_OAUTH2_ALLOW_UNVERIFIED_LEGACY_UID_MIGRATION = False
```

Configured historical keys supplement the backend’s built-in keys, and work
with an explicit current `ID_KEY`. Custom backends must implement extraction
for their historical keys when identifiers require transformations or scoping.

If an email or username has changed and its old value is no longer returned,
indexed lookup cannot recover the old association. There is no global JSON
search fallback. Use account recovery or an authenticated linking flow.
Presenting a reclaimed mutable identifier with conflicting stored evidence
always fails, including when unverified migration is enabled.

<a id="compatibility-policy"></a>

## Compatibility policy

`ALLOW_UNVERIFIED_LEGACY_UID_MIGRATION` defaults to enabled only for the
specific built-in transitions marked “Yes” below. It requires the audited
backend class and current built-in key. Custom subclasses, newly added legacy
keys, and other configuration-driven transitions require evidence by default.
Explicitly selecting the same built-in current key retains that transition’s
default. Other current keys do not.

The global `SOCIAL_AUTH_ALLOW_UNVERIFIED_LEGACY_UID_MIGRATION` setting and its
backend-specific variants override that default. Explicit `False` always
requires evidence. Explicit `True` permits missing evidence even outside the
audited list, but cannot override conflicting or invalid evidence.

#### WARNING
Allowing missing evidence accepts the risk that a new owner of an old email,
username, or other mutable identifier can claim its association. The
compatibility policy preserves that behavior for known historical setups;
it does not make those migrations safe. Disable it before upgrading if this
risk is unacceptable.

<a id="historical-identifier-audit"></a>

<a id="historical-backend-audit"></a>

## Historical backend audit

This table describes default storage declarations in social-core 5.2.0 for the
25 built-in backend variants that now declare historical identifier keys.
Three stored the new identifier directly; Fence and CAS stored `sub` under
`id`. The remaining 20 lacked a directly usable stable identifier field.
Stored ID tokens are not decoded as migration evidence. Actual data can differ
because of older releases, custom pipelines, or extra-data configuration.

#### Historical transitions and default compatibility allowances

| Provider                   | Old key              | Current key    | Stored evidence   | Unverified default   |
|----------------------------|----------------------|----------------|-------------------|----------------------|
| `arcgis`                   | `username`           | `id`           | Absent            | Yes                  |
| `azuread-oauth2`           | `upn`                | `sub`          | Absent            | Yes                  |
| `azuread-oauth2-v2`        | `upn`                | `sub`          | Absent            | Yes                  |
| `azuread-v2-tenant-oauth2` | `preferred_username` | `sub`          | Absent            | Yes                  |
| `cas`                      | `username`           | `sub`          | Alias `id`        | No                   |
| `cognito`                  | `username`           | `sub`          | Absent            | Yes                  |
| `dailymotion`              | `username`           | `id`           | `id`              | No                   |
| `deezer`                   | `name`               | `id`           | Absent            | Yes                  |
| `discourse`                | `email`              | `external_id`  | Absent            | Yes                  |
| `fence`                    | `username`           | `sub`          | Alias `id`        | No                   |
| `google-oauth`             | `email`              | `id`           | Absent            | Yes                  |
| `google-oauth2`            | `email`              | `sub`          | Absent            | Yes                  |
| `google-onetap`            | `email`              | `sub`          | Absent            | Yes                  |
| `google-openidconnect`     | `email`              | `sub`          | Absent            | Yes                  |
| `keycloak`                 | `username`           | `sub`          | Absent            | Yes                  |
| `mailru`                   | `email`              | `id`           | Absent            | Yes                  |
| `okta-oauth2`              | `preferred_username` | `sub`          | Absent            | Yes                  |
| `okta-openidconnect`       | `preferred_username` | `sub`          | Absent            | Yes                  |
| `opensuse`                 | `nickname`           | `identity_url` | Absent            | Yes                  |
| `qiita`                    | `id`                 | `permanent_id` | `permanent_id`    | No                   |
| `scistarter`               | `email`              | `profile_id`   | `profile_id`      | No                   |
| `trello`                   | `username`           | `id`           | Absent            | Yes                  |
| `tumblr`                   | `name`               | `uuid`         | Absent            | Yes                  |
| `ubuntu`                   | `nickname`           | `identity_url` | Absent            | Yes                  |
| `yandex-openid`            | `email`              | `identity_url` | Absent            | Yes                  |

<a id="django-historical-backfill"></a>

## Django historical backfill

The Django data migration records historical keys using frozen pre-v7 defaults.
It changes neither UIDs nor account ownership. Configure
`SOCIAL_AUTH_OLD_ID_KEYS` before running it if previous settings selected a
different identifier. This includes Google `USE_UNIQUE_USER_ID` and Qiita
`IDENTIFIED_BY_PERMANENT_ID`. For example:

```default
SOCIAL_AUTH_OLD_ID_KEYS = {
    "google-oauth2": "sub",
    "qiita": "permanent_id",
    "custom-provider": "old_subject",
    "trello": None,
}
```

Unknown and skipped providers retain empty keys. Authentication considers them
only through indexed UID candidates and the same evidence policy. A historical
key describes how existing associations were created; current settings alone
cannot establish it. See [Django Framework](django.html.md) for deployment order and migration costs.
