Fix: Turnkey WebAuthn rp.id Origin Mismatch (NotAllowedError)
When implementing non-custodial embedded passkey wallets with infrastructure providers such as Turnkey, Privy, or Dynamic, developers frequently encounter abrupt credential registration failures when moving code between local development, staging environments, and production domains:
SecurityError: The relying party ID is not a registrable domain suffix of the current origin.
at TurnkeyClient.createPasskey (turnkey-sdk/src/passkey.ts:88:21)
at HTMLButtonElement.handleRegister (app.dapp.xyz/signup.js:34:11)
[WebAuthn Fatal]: Failed to execute 'create' on 'CredentialsContainer':
DOMException: The operation either timed out or was not allowed. (NotAllowedError)
The browser refuses to prompt the user for Touch ID, Face ID, or Windows Hello. The operation either aborts instantly with a SecurityError or times out with NotAllowedError.
The root cause of this failure mode is a violation of the W3C WebAuthn Relying Party Identifier (rp.id) security policy: the value supplied for rp.id does not match the effective domain of the document origin, or it violates public suffix list (PSL) rules on multi-tenant hosting platforms (such as vercel.app or github.io).
If you are resolving Safari-specific passkey session persistence errors caused by Intelligent Tracking Prevention, review our guide on Safari ITP Storage Access Blocking Passkey Smart Wallets.
Architectural Breakdown: WebAuthn Relying Party Constraints
The WebAuthn standard enforces strict origin binding to prevent credential phishing across disparate websites.
The W3C rp.id Rules
Under section 5.4.2 of the WebAuthn Level 2 / Level 3 specification:
- Exact Match: The
rp.idmay match the current document’s hostname (e.g. Origin:https://wallet.example.com$\rightarrow$rp.id: "wallet.example.com"). - Registrable Domain Suffix: The
rp.idmay be any registrable domain suffix of the current host (e.g. Origin:https://wallet.example.com$\rightarrow$rp.id: "example.com"). - Forbidden Schemes & Ports: The
rp.idMUST NOT include protocols (https://), port numbers (:3000), or paths (/login). - Public Suffix List (PSL) Ban: The
rp.idMUST NOT be a bare effective top-level domain (e.g.com,org,co.uk, or multi-tenant domains likevercel.apporpages.dev). - IP Addresses: WebAuthn forbids domain suffixes on raw IP addresses. On
http://192.168.1.50:3000,rp.idmust be omitted or match the raw IP string exactly.
Step-by-Step Resolution Protocol
To eliminate rp.id mismatches across localhost, preview branches, and production deployments, implement this three-part dynamic configuration framework.
1. Implement Dynamic Environment-Aware rp.id Resolver
Instead of hardcoding a static domain string into your Turnkey or WebAuthn configuration, compute the valid rp.id dynamically at runtime:
// src/utils/getWebAuthnRpId.ts
export function getValidRelyingPartyId(): string {
if (typeof window === 'undefined') return 'localhost';
const hostname = window.location.hostname;
// 1. Local Development
if (hostname === 'localhost' || hostname === '127.0.0.1') {
return 'localhost';
}
// 2. Multi-tenant preview deployments (e.g., your-branch.vercel.app)
// On vercel.app, you CANNOT use 'vercel.app' as rp.id because it is in the PSL.
// You must use the full subdomain hostname.
if (hostname.endsWith('.vercel.app') || hostname.endsWith('.pages.dev')) {
return hostname;
}
// 3. Custom Production Domain (e.g., app.layerzero.com -> layerzero.com)
const parts = hostname.split('.');
if (parts.length >= 2) {
// Extract root apex domain (last two segments, e.g. "dapp.xyz")
return parts.slice(-2).join('.');
}
return hostname;
}
2. Configure Turnkey Passkey Client with Dynamic rp.id
Integrate the dynamic resolver into your Turnkey SDK initialization:
// src/auth/turnkeyClient.ts
import { TurnkeyClient } from '@turnkey/http';
import { createPasskeyService } from '@turnkey/sdk-browser';
import { getValidRelyingPartyId } from '../utils/getWebAuthnRpId';
export function initializeTurnkeyPasskeyService() {
const currentRpId = getValidRelyingPartyId();
console.log(`[TurnkeyGuard] Initializing WebAuthn with rp.id: ${currentRpId}`);
return createPasskeyService({
rp: {
id: currentRpId,
name: 'LayerZeroFault Secure Wallet',
},
// Enforce required user verification for smart contract compatibility
userVerification: 'required',
});
}
3. Cross-Subdomain Passkey Configuration (.well-known/webauthn)
If your architecture utilizes multiple distinct origins (e.g. auth.dapp.xyz, app.dapp.xyz, and pay.dapp.xyz), publish a WebAuthn Related Origins manifest at the apex domain to permit cross-origin passkey assertions:
// public/.well-known/webauthn
{
"origins": [
"https://auth.dapp.xyz",
"https://app.dapp.xyz",
"https://pay.dapp.xyz"
]
}
Note: The .well-known/webauthn file must be served with the Content-Type: application/json header and accessible without redirects over HTTPS.
Environment Configuration Reference Table
| Target Environment | Current Hostname | Valid rp.id Options | Invalid rp.id Values (Will Error) |
|---|---|---|---|
| Localhost | localhost:3000 | "localhost" | "http://localhost", "127.0.0.1:3000" |
| Vercel Preview | pr-42.dapp.vercel.app | "pr-42.dapp.vercel.app" | "vercel.app" (Blocked by PSL) |
| Subdomain | app.dapp.xyz | "app.dapp.xyz", "dapp.xyz" | "xyz", "https://dapp.xyz", "other.com" |
| Apex Production | dapp.xyz | "dapp.xyz" | "www.dapp.xyz" |
Frequently Asked Questions
Q: Why can’t I use vercel.app as my rp.id across all preview deployments?
The Public Suffix List (PSL) designates vercel.app as a public suffix to prevent unrelated users from reading cookies or claiming passkeys registered by other Vercel users. The browser enforces this by rejecting any rp.id matching a PSL entry.
Q: What happens to user passkeys if I change my domain name?
Passkeys are cryptographically anchored to the rp.id under which they were generated. If you change your domain from oldbrand.com to newbrand.com, existing passkeys will not be found by the browser on the new domain. You must implement a seed or email-based recovery mechanism to allow users to register a new passkey under the new domain.
Q: Does rp.id case-sensitivity matter?
Yes. The W3C specification mandates lowercase ASCII representations for Relying Party IDs. Never pass capitalized characters (e.g. DApp.xyz) to the WebAuthn API.