Resolving WebAuthn SecurityError (RP ID and Origin)
Unlike NotAllowedError, a SecurityError from navigator.credentials.create() or .get() is never ambiguous and never the user’s fault: it means the ceremony’s RP ID is not a registrable-suffix match of the page’s origin, the page is not a secure context, or a cross-origin iframe attempted WebAuthn without permission. This page turns each cause into a deterministic check and a fix. It sits under Debugging WebAuthn Protocol Errors; the RP ID rules themselves come from the relying party and authenticator roles cluster.
Cause Signatures
| Cause | Reproduces when | Fix |
|---|---|---|
| RP ID not a suffix of origin | rpId is not equal to / parent of location.hostname |
Set rpId to the shared registrable domain |
| Insecure origin | Page served over HTTP (not localhost) |
Serve over HTTPS |
| Cross-origin iframe | WebAuthn called in an iframe without publickey-credentials-* permission policy |
Add the permission policy or move out of the iframe |
| Trailing-dot / case mismatch | rpId has a trailing dot or differing case |
Normalise rpId to lowercase, no trailing dot |
Every row is deterministic — the same input always fails — which is why a SecurityError in production is a config bug to alert on, not a transient the user should retry.
Root Cause Analysis
1. RP ID granularity mismatch. The most frequent cause: rpId is set to example.com while the page runs on app.example.com (valid — example.com is a parent), or rpId is app.example.com while the page is example.com (invalid — a child is not a suffix of the parent’s page). The rule (WebAuthn L2 §5.1.3): rpId must equal the page’s effective domain or be a registrable parent of it.
2. Non-secure context. WebAuthn only runs in a secure context. Any HTTP origin except http://localhost throws SecurityError immediately.
3. Cross-origin embedding. An <iframe> on a different origin cannot call WebAuthn unless the embedding page grants publickey-credentials-get / publickey-credentials-create via Permissions Policy.
4. Normalisation slips. A trailing dot (example.com.) or uppercase in rpId fails the suffix comparison.
Step-by-Step Resolution
Step 1 — Validate rpId against the origin deterministically
function rpIdIsValidForOrigin(rpId: string, origin: string): boolean {
const host = new URL(origin).hostname.toLowerCase();
const id = rpId.toLowerCase().replace(/\.$/, ''); // strip trailing dot
return host === id || host.endsWith(`.${id}`);
}
// rpIdIsValidForOrigin('example.com', 'https://app.example.com') === true
// rpIdIsValidForOrigin('app.example.com', 'https://example.com') === false → SecurityError
Step 2 — Set rpId to the shared registrable domain
// Serve WebAuthn from app.example.com and accounts.example.com under one RP ID:
const rpID = 'example.com';
Step 3 — Enforce a secure context
Serve production over HTTPS; use localhost (not a LAN IP) for development. Confirm window.isSecureContext === true before calling WebAuthn.
Step 4 — Grant iframe permission or avoid embedding
<!-- Only if WebAuthn must run inside a cross-origin iframe -->
<iframe src="https://auth.example.com" allow="publickey-credentials-get; publickey-credentials-create"></iframe>
Step 5 — Alert, don’t retry
Route SecurityError to engineering telemetry, not a user retry, per the client-side debugging cluster.
Verification and Testing
// Unit-test the suffix rule
expect(rpIdIsValidForOrigin('example.com', 'https://example.com')).toBe(true);
expect(rpIdIsValidForOrigin('example.com', 'https://sub.example.com')).toBe(true);
expect(rpIdIsValidForOrigin('example.com', 'https://example.org')).toBe(false);
expect(rpIdIsValidForOrigin('app.example.com', 'https://example.com')).toBe(false);
In the browser, window.isSecureContext must be true. Reproduce the iframe case by embedding your auth page cross-origin without the allow attribute and confirming the SecurityError, then add the permission policy and confirm it clears. A synthetic monitor should assert no SecurityError occurs on any production sign-in surface.
Pitfalls
1. Setting rpId to a subdomain of the page. A child domain is not a suffix of the page’s parent origin — use the shared registrable domain.
2. Testing over a LAN IP. Only localhost is a secure context without HTTPS; a 192.168.x.x origin throws SecurityError.
3. Retrying SecurityError. It is deterministic — fix the config, do not loop.
Related
- Debugging WebAuthn Protocol Errors — the parent triage reference and rpIdHash decoding
- Fixing InvalidStateError on Registration — the sibling registration error class
- Relying Party and Authenticator Roles — the RP ID and origin-binding rules behind this error