How to Migrate WebAuthn (YubiKey) Credentials Between Keycloak Instances

Hello Keycloak community,

I’m facing a challenge migrating a user with a WebAuthn (YubiKey) credential between two identical Keycloak instances (test1.company.dev and test2.company.dev). Both instances are configured with the same RPID/sub-domain for WebAuthn policies.

Problem:

  1. I registered a YubiKey for a user on test1.company.dev, and it works correctly.

  2. I exported this user’s data into a users.json file (including their password and webauthn-passwordless credentials).

  3. I performed a partial realm import of this user into test2.company.dev via the Admin API. The user and both their credentials appear in the test2 admin console.

  4. However, when I try to log in to test2 with the YubiKey, I get errors saying that the YubiKey isn’t connected or is connected to a different user.

My Understanding & Suspicions:

It seems the credentialId of the WebAuthn token might be strongly tied to Keycloak’s internal user.id. Even though I’m importing the user with the same id from the source instance, the target instance might be having a conflict or mismatch in this internal mapping.

Specific Questions:

  1. Is the user’s id from the import JSON directly linked to the WebAuthn credentialId in a way that causes this conflict during migration?

  2. What is the recommended, robust method for programmatically migrating users with existing WebAuthn credentials between identical Keycloak instances?

  3. Are there specific fields within the WebAuthn credential data (e.g., the id field for the credential, counter) that need special handling or omission during such an import?

  4. Could this error suggest a credentialId collision in test2.dev’s database, possibly from a prior failed attempt?

Any insights or best practices for ensuring smooth WebAuthn credential migration would be highly appreciated. My goal is to avoid requiring users to re-register their YubiKeys when moving between Keycloak environments.
Note that I am operating in a highly partitioned environment and so need to have multiple instances of KC running separately and then manually sync. This is the part of the manual sync that I am having problems smoketesting.

Thank you!

User Credentials Format that I am using:
{

“id”: “SOME_HASH”,

“type”: “webauthn-passwordless”,

“createdDate”: 1764194058611,

“secretData”: “{}”,

“credentialData”: “{\“aaguid\”:\“00000000-0000-0000-0000-000000000000\”,\“credentialId\”:\“COPIED_FROM_TEST1.COMPANY.DEV\”,\“counter\”:5,\“credentialPublicKey\”:\“COPIED_FROM_TEST1.COMPANY.DEV\”,\“attestationStatementFormat\”:\“none\”,\“transports\”:[\“usb\”]}”

    }

  \]

When Im not mistaking, the Credential is bound to the DNS name.

You can check with this plugin: https://chromewebstore.google.com/detail/webauthn-devtools/ogpaejcbmlcjnkcbnfmdfheifgodnnec

When I check my Keycloak instance on my Homeserver, Keycloak is calling credentials.get() with payload:
{
“rpId”: “keycloak.<MY_DOMAIN>”,
“challenge”: “Am_0QdXOR2Wc…..”,
“userVerification”: “required”
}

The response does contain the origin, which is my Keycloak domain.

From a little bit of googling and checking the Keycloak config, you should be able to do this: Passkeys and custom domains | Okta Developer
Add your company.devas the rpId. Keycloak → Authentication → Policies → Webauthn Passwordless Policy → Relying Party ID

You will probably need to recreate your passkey afterwards, but migration should in theory work. I have not had this scenario.

Would be nice, if you come back and share your experience !

Correct. Passkeys (Fido2 Webauthn Keys, or however you call them), are domain-bound. That’s the main feature to make them phishing-resistant. The browser will only suggest and offer to use the keys of the current domain. Other keys MUST NOT being able to use.
While it’s possible to issue keys to multiple domains, it’s not possible to extend the valid domains afterwards. Once the key is created, it’s immutable.
So, if you switch domains, the current keys become useless/invalid.