Passkeys and getting token

I previously asked about the authentication methods.

The user_handle parameter in the request payload is not intended to be set by the client. Auth0 generates this value automatically when an authentication method is created, ensuring each method has a unique, immutable identifier within the system. Any value you pass in the request is ignored by design, and Auth0 always returns its own generated handle in the response.

Let’s imagine that a user wants to create a passkey.

a) The system first needs to create PublicKeyCredentialCreationOptions. This option data is created in our system / master system.

As far as I understand, the user.id field in these options should refer to the master system’s user ID, rather than to a user ID from an external system such as Auth0.

At this point, the Auth0 user_handle is not even known because the authentication method has not yet been created. Is my understanding correct?

b) The user then creates a credential (passkey) based on the previously generated PublicKeyCredentialCreationOptions.

The credential data is sent to our backend (the master system). Once the passkey data has been successfully inserted into our database, we create the corresponding authentication method via the Auth0 Management API.

Our requirement is that, regardless of what happens with Auth0 or any other external identity provider, we must always be able to recreate or provision all passkeys in another provider’s system if necessary—for example, in an emergency or migration scenario.

I am not a WebAuthn expert, but this seems rather problematic if we cannot use the user_handle that is already used by our master database. I also just don’t understand why the Auth0 documentation presents user_handle as part of the POST authentication-method request:
Auth0 — POST /api/v2/users/{id}/authentication-methods

How Auth0 internally find correct public key when requesting token? Using userHandle and credential id?

POST /oauth/token

Here, our problem is that we can’t get the login flow to work or retrieve the token data. The API returns an unauthorized response. My best guess is that the user_handle doesn’t match.

Hi @DuxXXXX

I understand that you are building a custom passkey authentication system where your master system generates PublicKeyCredentialCreationOptions and stores passkey credentials independently. You want to ensure that if Auth0 becomes unavailable, you can recreate all passkeys in another provider’s system. However, Auth0 auto-generates the user_handle value and ignores any client-provided value, which creates a problem: you cannot use your master system’s user ID as the user_handle, and you do not know Auth0’s generated user_handle until after the authentication method is created. Additionally, your token requests to POST /oauth/token are returning unauthorized, and you suspect the user_handle mismatch is the cause.

Auth0’s user_handle is an internal identifier that Auth0 generates and manages automatically. It is not intended for client-side integration with external systems. The user_handle parameter appears in the API documentation because it is part of the authentication method object structure, but client-provided values are always ignored. This design prevents clients from accidentally creating credential conflicts or security issues. However, this creates a legitimate architectural challenge for disaster recovery and multi-provider scenarios.

[ROOT CAUSE]

Auth0’s WebAuthn implementation follows the WebAuthn specification, which defines user_handle as an opaque byte sequence that the authenticator (e.g., OS passkey manager) uses to identify the user during credential creation and assertion. The WebAuthn spec allows the relying party (Auth0) to choose how to generate and manage this value.

Auth0’s design decisions:

  1. user_handle is auto-generated: Auth0 generates a unique user_handle for each authentication method to ensure uniqueness and prevent credential collisions. This value is immutable and cannot be overridden by the client.

  2. Client-provided values are ignored: Any user_handle value you include in the POST /api/v2/users/{id}/authentication-methods request is silently ignored. Auth0 always returns its own generated value in the response.

  3. user_handle is not exposed via Management API: While the authentication method object includes user_handle, there is no dedicated endpoint to retrieve it after creation. You must store it from the creation response if you need to reference it later.

  4. Credential matching during token requests: When a user attempts to authenticate via POST /oauth/token with a passkey, Auth0 matches the credential using a combination of:

    • The credential ID (a unique identifier for the specific passkey)
    • The user_handle (to identify which user the credential belongs to)
    • The signature verification (to confirm the credential is valid)

If the credential ID or user_handle does not match Auth0’s records, the authentication fails with unauthorized.

Why your token request is failing:

Your POST /oauth/token request is returning unauthorized because:

  1. Credential ID mismatch: The credential ID you are sending does not match the credential ID Auth0 stored when the authentication method was created.
  2. User handle mismatch: The user_handle embedded in your passkey assertion does not match the user_handle Auth0 generated and stored.
  3. Credential not found: The credential does not exist in Auth0’s database for the user you are attempting to authenticate.

[Recommended Approach]

If your requirement is to maintain the ability to recreate passkeys in another provider in case of emergency, you must implement a credential escrow pattern:

Step 1: Store Auth0-generated user_handle in your master system

When you create an authentication method via POST /api/v2/users/{id}/authentication-methods, Auth0 returns the generated user_handle in the response:

{
  "id": "dev_abc123...",
  "user_id": "auth0|user123",
  "user_handle": "auth0_generated_handle_xyz",
  "credential_id": "credential_xyz",
  "created_at": "2024-01-15T10:00:00Z",
  "...": "..."
}

Step 2: Store this response in your master system

// After creating the authentication method in Auth0
const authMethod = await managementApi.post(
  `/api/v2/users/${userId}/authentication-methods`,
  {
    type: "webauthn-platform",
    transports: ["internal"],
    credential_data: credentialData
  }
);

// Store the entire response in your master database
await masterDb.saveCredentialEscrow({
  user_id: masterSystemUserId,
  auth0_user_id: userId,
  auth0_user_handle: authMethod.user_handle,
  auth0_credential_id: authMethod.credential_id,
  credential_public_key: credentialPublicKey, // Store the public key
  created_at: authMethod.created_at
});

Step 3: For disaster recovery, use the stored public key

If you need to migrate to another provider, you have the credential’s public key (which you stored), but you will need to recreate the credential registration in the new provider’s system. The user_handle from Auth0 is not portable—each provider generates its own user_handle. Instead, use your master system’s user ID as the user_handle in the new provider.

Step 4: Do not attempt to override Auth0’s user_handle

Do not try to set user_handle to your master system’s user ID when creating credentials in Auth0. Auth0 will ignore it, and you will create a mismatch between what you think the user_handle is and what Auth0 actually stored.

Addressing the token request failure:

To debug why your POST /oauth/token request is failing:

  1. Verify the credential exists in Auth0:
curl -X GET "https://YOUR_DOMAIN/api/v2/users/USER_ID/authentication-methods" \
  -H "Authorization: Bearer YOUR_MANAGEMENT_API_TOKEN"
  1. Confirm that the credential ID from your passkey assertion matches a credential in this list.
  2. Verify the credential type:
    Ensure the credential type is webauthn-platform (for platform authenticators like Windows Hello or Touch ID) or webauthn-roaming (for security keys). The type must match how the credential was created.
  3. Verify the assertion is being sent correctly:
    When calling POST /oauth/token, ensure you are sending the WebAuthn assertion in the correct format. Auth0 expects the assertion to include:
    • The credential ID
    • The authenticator data
    • The client data JSON
    • The signature
  4. Check Auth0 logs:
    Review your Auth0 tenant logs (Dashboard → Logs) for the failed authentication attempt. The logs may provide additional error details about why the credential was not recognized.

Kind Regards,
Nik

Thanks for the answers, @nik.baleca :+1:

They confirmed my initial hunch that Auth0 does not support this kind of use case.

If we decide to implement passkeys without allowing them to be transferable, there is another issue.

As I mentioned earlier, we currently generate the PublicKeyCredentialCreationOptions on our side. At that point, we don’t know the Auth0 user’s user_handle.

It feels a bit unnecessary to create a dummy authentication method just to obtain Auth0’s internal user_handle? Here, my assumption is that the user identity ID should match the user_handle in options data.

It would be nice if there were an endpoint that could return the PublicKeyCredentialCreationOptions, something along the lines of:
“We want to start passkey creation for user X. Generate the options using these values for the authenticator selection criteria etc: resident key, user verification, and hints.”

There might already be an endpoint that supports this, but I may have simply missed it.

Hi again @DuxXXXX

After doing some further investigation on the matter, I can confirm that the Authentication API does not allow passing these parameters to the request in order to store them on your external system.

I understand that this is not ideal for your specific use case, unfortunately, the only recommendation I can provide regarding this would be to submit a feedback card under the Auth0 Community in order to pass this information along to our engineers.

Kind Regards,
Nik