Skip to content

Migrating passkeys to a new domain

Passkeys are resistant to phishing attacks because they’re bound to a specific domain (RP ID). This does, however, raise some serious issues:

  1. Domain migration - at some point you may need to change your domain. If your passkeys are registered to oldsite.com, how will your users sign in on newsite.com?

  2. Related domains - users may legitimately want to present a passkey from a different domain e.g. presenting an example.com passkey to example.co.uk.

Fortunately the passkey specs now support Related Origin Requests, allowing you to accept passkeys bound to other domains.

For the examples we’ll use two domains: oldsite.com and newsite.com.

Assume existing passkeys are registered to oldsite.com, and you want to migrate your domain to newsite.com. You have a couple of options:

Continue using the existing Relying Party ID on the new domain

Section titled “Continue using the existing Relying Party ID on the new domain”

The simplest option is to continue using an RP ID of oldsite.com on newsite.com.

1. Authorize passkey ceremonies with the old RP ID

Section titled “1. Authorize passkey ceremonies with the old RP ID”

When your backend authorizes registration or authentication, keep passing oldsite.com as the RP ID:

backend/register.ts
const result = await passlock.authorizePasskeyRegistration({
rpId: "oldsite.com",
});
backend/authenticate.ts
const result = await passlock.authorizePasskeyAuthentication({
rpId: "oldsite.com",
});

Host a /.well-known/webauthn file at the root of oldsite.com (the RP ID domain), whitelisting newsite.com:

https://oldsite.com/.well-known/webauthn
{
"origins": [
"https://oldsite.com",
"https://newsite.com"
]
}

This is the simplest approach, for users and developers. However, there are some drawbacks:

  1. You need to host https://oldsite.com/.well-known/webauthn indefinitely.

  2. Some browsers/devices ignore the Relying Party Name and display the RP ID to users, so they will see the message “do you want to sign in with your oldsite.com passkey?” or similar.

Alternatively, you can use newsite.com for new passkeys, whilst continuing to accept passkeys registered to oldsite.com:

1. Register new passkeys with the new RP ID

Section titled “1. Register new passkeys with the new RP ID”

For new or replacement passkeys, register them with newsite.com instead of oldsite.com.

backend/registration.ts
const result = await passlock.authorizePasskeyRegistration({
rpId: "newsite.com",
});

Host a /.well-known/webauthn file at the root of oldsite.com, whitelisting newsite.com:

https://oldsite.com/.well-known/webauthn
{
"origins": [
"https://oldsite.com",
"https://newsite.com"
]
}

By default, you should authenticate passkeys with the new newsite.com RP ID. Newly created passkeys will work fine.

backend/authentication.ts
const result = await passlock.authorizePasskeyAuthentication({
rpId: "newsite.com",
})

To authenticate legacy oldsite.com passkeys you will need to pass oldsite.com as the RP ID:

backend/authentication.ts
const result = await passlock.authorizePasskeyAuthentication({
rpId: "oldsite.com",
})

This tells Passlock to fetch authentication options for oldsite.com, allowing the browser to present passkeys that were originally created for that RP ID.

It’s a good idea to prompt legacy users to create a new passkey, which will be registered against the new RP ID. The best time to do this is usually following a successful login with a legacy passkey:

backend/register.ts
const result = await passlock.authorizePasskeyRegistration({
rpId: "newsite.com",
})

This approach allows you to support passkeys registered to an old domain, while also registering passkeys against the new domain.

After a user has authenticated with a “legacy” passkey, you can prompt them to register a new one, using the current domain/RP ID. You can even delete the old passkey for them. The downsides are:

  1. You need to host https://oldsite.com/.well-known/webauthn until all users have been migrated.

  2. You still need to decide whether to request a passkey for the old or new RP ID. There is currently no single request that asks the browser to present passkeys from both domains.