The Turnkey WebAuthn ‘NotAllowedError: The relying party ID is not a valid domain string’ occurs when the rp.id specified in publicKeyCredentialCreationOptions does not match the effective domain or a registrable domain suffix of window.location.origin. In cross-origin iframe architectures, browsers strictly enforce origin boundaries. Configuring the Turnkey embedded wallet origin or proxying authentication through the parent domain fixes the mismatch.
Before executing state changes or signing transactions, verify your cryptographic parameters using our interactive WebAuthn Passkey Debugger utility.
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" |
[!TIP] Companion Diagnostic Guide: If experiencing related infrastructure failures, cross-reference symptoms with our technical breakdown on Safari ITP Blocking Passkey Smart Wallets Storage Access Rejection.
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.