Fix: WebAuthn publickey-credentials-create Permissions Policy Revert
With the rollout of updated browser security sandboxing in modern Chromium, Firefox, and Safari engines, developers integrating embedded passkey wallets (such as Privy, Turnkey, Dynamic, or Biconomy) frequently hit an abrupt failure during user onboarding:
SecurityError: Failed to execute 'create' on 'CredentialsContainer':
Access to this feature is blocked by permissions policy: publickey-credentials-create
at WebAuthnService.registerCredential (iframe.auth.provider.com/sdk.js:112:15)
at HTMLButtonElement.handleCreateWallet (dapp.xyz/embed.js:45:9)
[FATAL]: WebAuthn enrollment aborted. Platform authenticator denied iframe access.
Existing users can log in smoothly, but new users attempting to register their device passkeys via Face ID or Touch ID receive an immediate error before the browser authentication modal can appear.
The root cause of this failure mode is the W3C Permissions Policy divergence between credential retrieval and creation: historical documentation instructed developers to specify allow="publickey-credentials-get". However, modern browser specifications split credential creation into a separate, higher-privilege directive: publickey-credentials-create.
If you are diagnosing legacy cross-origin iframe delegation errors for credential retrieval, consult our companion guide on WebAuthn Cross-Origin Iframe Permissions Policy Fix.
Architectural Breakdown: Split WebAuthn Policy Directives
In early WebAuthn implementations, a single publickey-credentials or publickey-credentials-get permission governed all WebAuthn operations.
The W3C Policy Architecture
Under current W3C feature policy standards, browsers enforce granular separation:
publickey-credentials-get: Controlsnavigator.credentials.get(). Used for asserting existing passkeys (login, transaction signing, session authentication).publickey-credentials-create: Controlsnavigator.credentials.create(). Used exclusively for enrolling new public-key credentials into the hardware enclave.- The Sandbox Trap: When a parent document embeds an authentication frame via
<iframe allow="publickey-credentials-get">, the browser strictly denies the frame’s request to generate new cryptographic keys, throwing aSecurityErrororNotAllowedError.
Step-by-Step Resolution Protocol
To ensure seamless passkey registration and login across all desktop and mobile browsers, implement this three-part policy update.
1. Update Iframe Embed Attributes
Update all <iframe /> declarations embedding wallet signers or identity modules to delegate both create and get capabilities:
<!-- Recommended Secure Delegation for Web3 Embedded Wallets -->
<iframe
src="https://auth.yourwalletprovider.com/embedded"
title="Web3 Passkey Authentication Enclave"
allow="publickey-credentials-get https://auth.yourwalletprovider.com; publickey-credentials-create https://auth.yourwalletprovider.com"
class="w-full h-full border-0"
></iframe>
Note: For testing and universal SDK compatibility, you can supply wildcards (*), but explicit origin whitelisting is strongly recommended for production security.
2. Configure HTTP Permissions-Policy Headers
If your hosting platform (Vercel, Cloudflare Pages, Nginx, or AWS CloudFront) serves security headers, ensure the parent document does not override and restrict child iframe permissions:
Nginx Configuration
# /etc/nginx/conf.d/security-headers.conf
add_header Permissions-Policy "publickey-credentials-create=(self 'https://auth.yourwalletprovider.com'), publickey-credentials-get=(self 'https://auth.yourwalletprovider.com')" always;
Vercel Header Configuration (vercel.json)
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Permissions-Policy",
"value": "publickey-credentials-create=(self 'https://auth.yourwalletprovider.com'), publickey-credentials-get=(self 'https://auth.yourwalletprovider.com')"
}
]
}
]
}
3. Programmatic Feature Policy Detection in Client SDKs
In custom dApp components, inspect whether the current execution context holds the necessary policy delegations before prompting the user, providing a graceful fallback redirect if blocked:
// src/utils/assertWebAuthnPolicy.ts
export function assertWebAuthnCreationAllowed(): boolean {
if (typeof document === 'undefined') return true;
// Modern browser permissions policy inspection
const policy = (document as any).permissionsPolicy || (document as any).featurePolicy;
if (policy && typeof policy.allowsFeature === 'function') {
const allowsCreate = policy.allowsFeature('publickey-credentials-create');
const allowsGet = policy.allowsFeature('publickey-credentials-get');
if (!allowsCreate) {
console.warn(
'[WebAuthnGuard] Embedded context is missing publickey-credentials-create permission. ' +
'Passkey registration will fail. Fallback to top-level window redirect required.'
);
return false;
}
}
return true;
}
Browser Policy Support Matrix
| Browser Engine | publickey-credentials-get | publickey-credentials-create | Default in Sandboxed Iframe |
|---|---|---|---|
| Chrome (124+) | Supported | Enforced Strictly | Blocked without explicit allow |
| Firefox (126+) | Supported | Enforced Strictly | Blocked without explicit allow |
| Safari (17.4+) | Supported | Supported | Blocked unless delegated |
| Edge (Chromium) | Supported | Enforced Strictly | Blocked without explicit allow |
Frequently Asked Questions
Q: Why did older versions of Chrome allow passkey creation with only publickey-credentials-get?
During the transition phase of the WebAuthn specification, Chromium bundled both operations under publickey-credentials-get for backwards compatibility with early WebAuthn Level 1 implementations. This grace period was retired in recent engine versions.
Q: Does adding publickey-credentials-create introduce security risks to my dApp?
No. Delegating this capability merely allows the designated iframe to request the operating system’s hardware authenticator to generate a credential. The user must still physically authenticate (via biometric scan or PIN) before any key is created.
Q: What should I do if my dApp is embedded inside a third-party iframe I cannot control?
If your dApp is embedded inside an external portal or wallet aggregator that refuses to update its iframe allow attributes, detect the missing policy using assertWebAuthnPolicy and trigger window.open(authUrl, '_blank') to complete enrollment in an un-sandboxed popup window.