To provide smooth, in-context authentication across multiple domains, organizations often embed sign-in pages within iframes. However, loading authentication contexts inside third-party frames exposes users to critical threats like clickjacking (UI redressing) and unauthorized credential creation. To mitigate these risks, browsers disable WebAuthn in cross-origin iframes by default. Safely lifting this restriction requires active, defense-in-depth protocols.
Identify threat models
Before enabling passkeys (WebAuthn) inside subframes, understand the abuse scenarios you are defending against:
- Tracking using hidden iframe injection: An attacker triggers a WebAuthn prompt from their own domain using an ad or widget on a trusted site, tricking users into authorizing a passkey without seeing the context. This links the user's identity to an attacker-controlled account to harvest data.
- Visual overlay and clickjacking (UI redressing): A malicious parent page renders the authentication iframe invisible using standard CSS and overlays a fake UI element to steal a click that triggers an authentication flow. This can result in session hijacking or forced unauthorized actions if the user inadvertently completes the prompt.
To counter these threats, follow these best practices:
For the top-level document (top frame):
For the embedded document (iframe):
- Enable partitioned third-party cookies
- Guard the endpoint with Content Security Policy
- Trust, but verify server-side
For both documents:
Enable delegation using Permissions Policy
Browsers block access to WebAuthn in cross-origin iframes by default. Permissions Policy is the unified web platform mechanism that lets a top-level document explicitly delegate these powerful capabilities to specific, trusted third-party origins.
Feature tokens
WebAuthn uses two distinct tokens:
publickey-credentials-get: Grants authorization for passkey sign-in flows (navigator.credentials.get()).publickey-credentials-create: Grants authorization for passkey registration flows (navigator.credentials.create()).
Requirements for enablement
Enabling these capabilities requires alignment in both the parent server response and the client-side markup:
- Permissions-policy HTTP response header (parent server site): The parent page must declare the allowed origins in its HTTP response headers using Structured Fields syntax.
Permissions-Policy: publickey-credentials-get=(self "https://embedded-auth.example.com")
Permissions Policy: publickey-credentials-get compatibility:
Permissions Policy: publickey-credentials-create compatibility:
- The HTML
allowattribute: In the HTML markup, the<iframe>element must also declare that it enables the feature.
<iframe src="https://embedded-auth.example.com?nonce=deadbeef12345678&client=https%3A%2F%2Fembedded-auth.example.com" allow="publickey-credentials-get"></iframe>
iframe allow="publickey-credentials-get" compatibility:
Browser Support
iframe allow="publickey-credentials-create" compatibility:
Browser Support
Enable partitioned third-party cookies
To ensure a reliable authentication flow, a session must be established and maintained within the embedded cross-origin iframe. As modern browsers transitioned to strict third-party cookie restrictions, standard persistence mechanisms are often blocked by default and might require calling the Storage Access API to gain access.
To mitigate these obstacles, configure your session cookies with the SameSite:
None, Secure, and Partitioned attributes. This unified platform mechanism
ensures persistent state within the iframe while respecting browser-level
privacy controls.
Set SameSite: None
SameSite:
None
explicitly marks a cookie for cross-site access, letting it be sent with
requests made from a third-party context (like an iframe). This attribute is a
prerequisite for cookies to be functional in cross-origin scenarios, though it
must be combined with the Secure attribute to be accepted by modern browsers.
Set Partitioned
The Partitioned attribute opts the cookie into CHIPS (Cookies Having
Independent Partitioned
State),
letting the cookie be stored separately for each top-level site. This ensures
the cookie remains accessible within the specific third-party iframe context,
enabling persistent session state without enabling cross-site tracking. The user
will have to sign in again for each embed on a different site.
Guard the endpoint with Content Security Policy
While Permissions Policy determines if your iframe can run WebAuthn, Content Security Policy (CSP) determines who is allowed to host your iframe.
For an authentication endpoint, it is critical to ensure that only authorized partner sites or your own properties can load the login subframe, shutting down unauthorized clickjacking attempts before they can even load the UI.
Use frame-ancestors
The frame-ancestors
directive
defines the valid parent pages that can embed your site. By adding domains to
this directive, you can permit the domains that are allowed to embed the
login subframe.
Content-Security-Policy: frame-ancestors 'self' https://parent-site.example.com;
Content Security Policy: frame-ancestors compatibility:
Set X-Frame-Options
The legacy X-Frame-Options header supports similar capability, but only
supports binary options (DENY or SAMEORIGIN). Set both CSP frame-ancestors
and X-Frame-Options: DENY in case the browser doesn't support CSP. CSP is
always prioritized where it is supported.
X-Frame-Options: DENY
X-Frame-Options compatibility:
Trust, but verify server-side
The browser's client-side checks evaluate intent and permissions, but the server is the ultimate arbiter of trust. Verify the response on the Relying Party (RP) server to ensure the context is valid and signed.
Client-data payload
WebAuthn client data includes parameters specifically designed to help you verify the context of a request made within an iframe:
crossOrigin(boolean): Indicates if the WebAuthn API was invoked inside a cross-origin iframe. If your architecture relies on iframes, your server must enforce that this flag istrue.topOrigin(string): The origin of the top-level browsing context (what is visible in the browser's address bar). The server must verify this against a list of known, authorized parent origins.
Verification checklist
To verify the authenticator response on your server, perform the following steps:
- 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
Establish sessions securely using postMessage()
To establish a session reliably, the iframe must pass the authentication token
back to the parent page using postMessage(), letting the parent manage the
session state in its own first-party context.
Secure workflow
To establish a secure session, follow this workflow:
- Ensure the iframe
srcURL contains anonceandoriginquery parameters:- Use a random value for the
nonce. Anonceserves as a security verification token to ensure that the authentication token received from an iframe legitimately matches the specific session initiated by the parent page. - Use the parent frame domain for the
origin. Anoriginparameter specifies the origin of the parent page, enabling the iframe to securely identify the authorized context in which it has been embedded.
- Use a random value for the
- The iframe completes WebAuthn authentication with its own server.
The iframe server issues a token such as a JWT that includes the
nonceand forwards 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 page persists the session if the JWT is successfully verified.
The sender and receiver both share security responsibilities:
- The Sender (iframe): Always specify a strict target origin when sending
messages (never use
"*"). - The Receiver (parent): Always verify
event.originwhen receiving messages to prevent origin spoofing.
Conclusion
Safe iframe usage hinges on Permissions Policy for enablement, CSP for
restriction, partitioned third-party cookies for session persistence,
server-side verification of client context, and context-aware session handoff
using postMessage().
To learn more about related topics, follow Google's Chrome developer blog and explore more resources at the Chrome Developer Identity documentation.