DEV Community

Cover image for WebAuthn RP ID vs Origin: Fixing Cross-Domain Passkeys
Julian Halden
Julian Halden

Posted on Originally published at helpers.work

WebAuthn RP ID vs Origin: Fixing Cross-Domain Passkeys

If your passkey works on one hostname but fails on another, start by comparing the RP ID with the actual calling origin.

They are not interchangeable values. An RP ID is a domain identifier. An origin includes the scheme, hostname, and port.

This is part three of Passkeys in Production, adapted from the Helpers.work RP ID guide. We build the Passkey Deployment Checker, which evaluates this public configuration boundary without claiming to test the entire login flow.

One URL, three different values

For this illustrative page:

https://login.example.com/account/security
Enter fullscreen mode Exit fullscreen mode

You have:

Origin:   https://login.example.com
Hostname: login.example.com
RP ID:    potentially example.com or login.example.com
Enter fullscreen mode Exit fullscreen mode

The path is not part of the origin. A non-default port is part of the origin, but never part of the RP ID.

Registration options use publicKey.rp.id; authentication options use publicKey.rpId. The server's expected RP ID must agree with the credential's scope.

The browser origin https://login.example.com includes a scheme and host, while its eligible parent RP ID example.com is a bare domain.

The ordinary relationship is directional

Without Related Origin Requests, a caller can use its own effective domain or an eligible parent domain as the RP ID.

Caller RP ID Ordinary relationship
https://login.example.com example.com Eligible parent
https://login.example.com login.example.com Same host
https://example.com login.example.com Not an eligible parent
https://shop.example.com login.example.com Sibling, not parent
https://example.co.uk example.com Different domain

A public suffix such as com or co.uk is not an acceptable shared RP ID.

This is why "just remove the first subdomain" is not a sufficient validation algorithm. Public suffix handling needs standards-aware logic.

Sibling subdomains do not always need a JSON file

If accounts.example.com and shop.example.com both use credentials registered for example.com, the ordinary relationship can be enough.

You still need:

  1. Credentials created for that exact RP ID.
  2. Correct shared account and credential mappings.
  3. Explicit server acceptance of the intended origins.

A broad RP ID does not authorize every subdomain in your backend. Treat delegated, abandoned, and user-controlled subdomains as part of the threat model.

Cookies and shared sessions remain separate concerns.

Different domains need explicit authorization

Suppose the browser runs at https://example.co.uk but requests RP ID example.com.

A supporting browser can use Related Origin Requests if the RP ID domain publishes:

https://example.com/.well-known/webauthn
Enter fullscreen mode Exit fullscreen mode

For example:

{
  "origins": ["https://example.co.uk", "https://login.example.net"]
}
Enter fullscreen mode Exit fullscreen mode

These are illustrative origins. Serve your actual document publicly as JSON and list only relationships you intend to trust.

The location matters: putting this file on example.co.uk does not authorize use of the example.com RP ID.

The server still verifies the actual origin

The public allowlist permits the browser to request the credential. It does not replace the relying party's verification policy.

For the example above, the server must explicitly accept https://example.co.uk, verify the expected challenge and RP ID context, and validate the appropriate cryptographic response.

Do not fix a mismatch by accepting arbitrary origins. CORS headers and cookie-domain changes also do not substitute for WebAuthn authorization.

Cross-domain flow: example.co.uk requests RP ID example.com, the related origins file is served on example.com, and the backend still verifies the caller.

Five labels is not five URLs

The compatibility procedure counts registrable domain labels:

  • example.com contributes example.
  • example.co.uk also contributes example.
  • login.example.net also contributes example.
  • example-rewards.com contributes example-rewards.

Supporting implementations must handle at least five distinct labels; they may support more. The checker evaluates that five-label baseline.

Order matters when a client exhausts its label budget. A listed origin can still fall beyond the supported limit.

Do not publish a giant list and assume every client will process all of it. For many unrelated tenant domains, a central sign-in domain may be the more maintainable design.

Debug in this order

First, inspect the actual origin where WebAuthn runs. A redirect from a marketing page can change the relevant hostname.

Second, inspect the backend-generated options. Do not assume the RP ID was discovered by a scanner; the Helpers.work tool defaults to the hostname when the RP ID is omitted.

Third, determine whether ordinary parent-domain eligibility is enough. If not, inspect the RP ID host:

curl --proto '=https' --max-time 15 --silent --show-error --include \
  https://example.com/.well-known/webauthn
Enter fullscreen mode Exit fullscreen mode

Replace the example domain with yours. Look for HTML fallbacks, login challenges, incorrect content types, or missing origins.

Finally, test browser support and server verification. Basic WebAuthn support does not guarantee Related Origin Requests support. A SecurityError is a diagnostic clue, not a complete root-cause report.

Domain changes are credential migrations

Existing credentials do not change RP ID when you update configuration.

Before renaming an authentication domain, decide whether to retain the old RP ID through a supported related-origin flow or guide users through authenticated re-enrollment. Keep a tested path for unsupported clients.

Continue with reading the checker report. For the broader rollout, see the deployment checklist and passkeys overview.

References: W3C related origins and web.dev's implementation guide.

Top comments (0)