Why WebAuthn is blocked in cross-origin iframes
Many organizations embed sign-in pages in iframes to provide in-context authentication across domains. But by default, browsers block WebAuthn in cross-origin iframes. The reason: allowing authentication flows in third-party frames creates two serious attack surfaces.
- Hidden iframe injection for tracking: An attacker can embed their own WebAuthn prompt inside an ad or widget on a trusted site. A user who unknowingly authorizes a passkey links their identity to an attacker-controlled account, enabling data harvesting.
- UI redressing (clickjacking): A malicious parent page can render an authentication iframe invisible with CSS and overlay fake UI. A single misdirected click can trigger an authentication flow, leading to session hijacking or forced unauthorized actions.
Lifting the WebAuthn iframe restriction requires defense-in-depth. You need coordinated protections at the top-level document, the embedded document, and the server.
Delegate WebAuthn with Permissions Policy
Permissions Policy is the unified mechanism for a top-level document to explicitly delegate powerful capabilities to trusted third-party origins. WebAuthn uses two distinct feature tokens:
publickey-credentials-get: authorizes passkey sign-in vianavigator.credentials.get().publickey-credentials-create: authorizes passkey registration vianavigator.credentials.create().
Delegation requires changes in both the parent server response and the client-side markup. In the HTTP response header, the parent declares allowed origins using Structured Fields syntax:
Permissions-Policy: publickey-credentials-get=(self "https://embedded-auth.example.com")
In the HTML markup, the <iframe> element must also declare the feature via the allow attribute:
<iframe src="https://embedded-auth.example.com?nonce=deadbeef12345678&client=https%3A%2F%2Fembedded-auth.example.com" allow="publickey-credentials-get"></iframe>
Both tokens are supported in Chromium-based browsers: publickey-credentials-get from Chrome 88 (and Edge 88), and publickey-credentials-create later in the same engines — 118 and 123 respectively in Firefox for the allow attribute. Safari support is not yet available.
Maintain a partitioned session
Authentication inside a cross-origin iframe needs a persistent session, but strict third-party cookie restrictions block standard persistence. The Storage Access API may grant access, but a simpler approach is to configure session cookies with SameSite=None, Secure, and Partitioned attributes.
SameSite=None marks a cookie for cross-site access so it is sent with requests from third-party contexts like an iframe. It is only accepted when combined with Secure.
The Partitioned attribute opts the cookie into CHIPS (Cookies Having Independent Partitioned State). This stores the cookie separately for each top-level site, keeping it accessible within the specific iframe context without enabling cross-site tracking. The trade-off: users must sign in again for each different site that embeds the frame.
Restrict who can host your frame with CSP
Permissions Policy controls whether your iframe can run WebAuthn. Content Security Policy controls who is allowed to host your iframe in the first place. For an authentication endpoint, you must ensure only authorized partner sites or your own properties can load the login subframe — shutting down clickjacking before the UI even renders.
The frame-ancestors directive defines valid parent pages that can embed your site:
Content-Security-Policy: frame-ancestors 'self' https://parent-site.example.com;
The legacy X-Frame-Options header provides similar protection but only supports DENY or SAMEORIGIN. Set both frame-ancestors and X-Frame-Options: DENY to cover browsers without CSP support — CSP takes precedence where it is supported.
X-Frame-Options: DENY
Verify context on the server
Client-side checks evaluate intent and permissions, but the server is the ultimate arbiter. The WebAuthn client data includes parameters specifically designed to verify iframe context:
crossOrigin(boolean): Indicates whether WebAuthn was invoked inside a cross-origin iframe. If your architecture relies on iframes, the server must enforce that this flag istrue.topOrigin(string): The origin of the top-level browsing context — what is visible in the address bar. The server must verify this against a known list of authorized parent origins.
When verifying the authenticator response on the Relying Party server:
- Parse and decode the signed
collectedClientDatafrom the authenticator response. - Ensure the
typematches the ceremony (webauthn.getorwebauthn.create). - Verify user presence and signature.
- If the request was intended to come from an iframe structure:
- Enforce
crossOrigin === true. - Enforce that
topOriginmatches your authorized list of parent origins.
- Enforce
Hand the session back with postMessage()
Once the iframe completes WebAuthn authentication, it must pass the authentication token to the parent page via postMessage() so the parent can manage session state in its own first-party context. The pattern requires care on both sides:
- The iframe
srcURL must containnonceandoriginquery parameters. Thenonce— a random value — verifies the token received matches the specific session the parent initiated. Theoriginparameter specifies the parent frame's domain, letting the iframe identify the authorized embedding context. - The iframe completes WebAuthn authentication with its own server.
- The iframe's server issues a token such as a JWT that includes the
nonceand forwards it to the parent page:
// Extract nonce and origin from the URL params
const urlParams = new URLSearchParams(window.location.search);
const nonce = urlParams.get('nonce');
const origin = urlParams.get('origin');
if (!nonce || !origin) {
alert('Nonce or origin is missing in the URL');
return;
}
// Create a JWT
const response = await post('/createToken', { nonce, origin });
const token = response.token;
// Post the JWT to the parent frame
window.parent.postMessage({ token }, origin);
- The parent page listens for the
messageevent, validates the sender origin, and verifies the token:
window.addEventListener("message", (event) => {
if (event.origin !== "https://embedded-auth.example.com") return;
// Verify the received JWT
const result = await post('/verifyIdToken', {
token: event.data.token,
origin: provider.origin,
});
});
- The parent persists the session if the JWT verifies successfully.
Both parties carry security responsibility. The sending iframe must always specify a strict target origin — never "*". The receiving parent must always verify event.origin to prevent origin spoofing.
Putting the Pieces Together
Safe passkey usage in iframes depends on layering several distinct security mechanisms. Each addresses a different part of the problem:
- Permissions Policy gates whether the iframe can access the WebAuthn API at all.
- CSP restricts what content can be embedded and from where, reducing the attack surface.
- Partitioned third-party cookies allow session state to persist inside the iframe without leaking across the top-level context.
- Server-side verification of the client context ensures that the relying party does not blindly trust the embedder.
- Context-aware session handoff via
postMessage()lets the embedded frame communicate results back to the parent without exposing the parent to unexpected message types.
This combination turns the iframe from a potential passkey interception point into a controlled, verifiable authentication surface. The critical rule is that session and credential state must never be shared implicitly based on the domain alone — every transfer of trust must be explicit and checked at each boundary.



