Passkeys within iframes

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):

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: publickey-credentials-get=(self "https://embedded-auth.example.com")

Permissions Policy: publickey-credentials-get compatibility:

Browser Support

  • Chrome: 88.
  • Edge: 88.
  • Firefox: not supported.
  • Safari: not supported.

Source

Permissions Policy: publickey-credentials-create compatibility:

Browser Support

  • Chrome: 88.
  • Edge: 88.
  • Firefox: not supported.
  • Safari: not supported.

Source

  • The HTML allow attribute: 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

  • Chrome: 84.
  • Edge: 84.
  • Firefox: 118.
  • Safari: not supported.

iframe allow="publickey-credentials-create" compatibility:

Browser Support

  • Chrome: not supported.
  • Edge: not supported.
  • Firefox: 123.
  • Safari: not supported.

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:

Browser Support

  • Chrome: 40.
  • Edge: 15.
  • Firefox: 58.
  • Safari: 10.

Source

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:

Browser Support

  • Chrome: 4.
  • Edge: 12.
  • Firefox: 4.
  • Safari: 4.

Source

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 is true.
  • 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:

  1. Parse and decode the signed collectedClientData from the authenticator response.
  2. Ensure the type matches the ceremony (webauthn.get or webauthn.create).
  3. Verify user presence and signature.
  4. If the request was intended to come from an iframe structure:
    • Enforce crossOrigin === true.
    • Enforce that topOrigin matches your authorized list of parent origins.

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:

  1. Ensure the iframe src URL contains a nonce and origin query parameters:
    • Use a random value for the nonce. A nonce serves 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. An origin parameter specifies the origin of the parent page, enabling the iframe to securely identify the authorized context in which it has been embedded.
  2. The iframe completes WebAuthn authentication with its own server.
  3. The iframe server issues a token such as a JWT that includes the nonce and 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);
    
  4. The parent page listens for the message event, 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,
      });
    });
    
  5. 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.origin when 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.