{
    "componentChunkName": "component---src-pages-blog-markdown-remark-fields-slug-js",
    "path": "/blog/webauthin-api",
    "result": {"data":{"markdownRemark":{"html":"<h1 id=\"what-is-the-webauthn-api\" style=\"position:relative;\"><a href=\"#what-is-the-webauthn-api\" aria-label=\"what is the webauthn api permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>What Is the WebAuthn API?</h1>\n<p>The Web Authentication API (WebAuthn) is a <a href=\"https://www.w3.org/TR/webauthn-3/\" target=\"_blank\" rel=\"nofollow\">W3C standard</a> that lets web applications authenticate users with public-key cryptography instead of passwords. It is the browser-facing half of the <a href=\"https://fidoalliance.org/fido2-2/fido2-web-authentication-webauthn/\" target=\"_blank\" rel=\"nofollow\">FIDO2 framework</a>, which pairs WebAuthn with the Client-to-Authenticator Protocol (CTAP) to enable passwordless, phishing-resistant login across browsers and platforms. WebAuthn reached W3C Recommendation status in March 2019, and Level 3 of the spec entered Candidate Recommendation in January 2026 with support across Chrome, Firefox, Edge, and Safari.</p>\n<p>The problem WebAuthn solves is structural. Password-based authentication relies on shared secrets: the server stores a representation of your password, and you prove your identity by reproducing it. That model breaks in predictable ways. Credential stuffing, password spraying, and database breaches all exploit the same fundamental weakness. Even layering traditional MFA on top only raises the bar; it doesn’t change the underlying architecture. Authenticator app codes and SMS tokens are still shared secrets that can be intercepted through phishing, SIM swapping, or social engineering.</p>\n<p>WebAuthn eliminates shared secrets entirely. When a user registers with a WebAuthn-enabled service, their device generates an asymmetric key pair. The private key stays on the device, locked inside a secure element or trusted platform module. The public key goes to the server. During authentication, the server issues a cryptographic challenge that only the correct private key can sign. No password crosses the wire. No secret is stored on the server to be stolen.</p>\n<p>Four core concepts define how WebAuthn operates:</p>\n<p><strong>Credential.</strong> A credential is the public-key pair bound to a specific user and a specific origin (the website’s domain). Each credential is scoped so it can only be used on the origin where it was created. This origin binding is what makes WebAuthn phishing-resistant; a cloned login page at a different domain simply cannot trigger the correct credential.</p>\n<p><strong>Attestation.</strong> During registration, the authenticator can provide an attestation statement: a cryptographic proof that the credential was created by a genuine, trusted device. Attestation lets the server verify the authenticator’s make and model, which matters in regulated environments where only hardware-backed keys are acceptable.</p>\n<p><strong>Assertion.</strong> During login, the authenticator produces an assertion: a signed response to the server’s challenge that proves the user possesses the private key. The server validates this signature against the stored public key to complete authentication.</p>\n<p><strong>Origin binding.</strong> The browser enforces that authentication requests are scoped to the exact origin (scheme + domain + port) that registered the credential. Even a pixel-perfect phishing site at a different URL will fail because the browser will not match credentials across origins. This protection operates at the protocol level, independent of user behavior or awareness.</p>\n<p>Together, these mechanisms create an authentication model where there is nothing to phish, nothing to replay, and nothing stored on the server worth stealing.</p>\n<h2 id=\"how-webauthn-works-registration-and-authentication-flows\" style=\"position:relative;\"><a href=\"#how-webauthn-works-registration-and-authentication-flows\" aria-label=\"how webauthn works registration and authentication flows permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>How WebAuthn Works: Registration and Authentication Flows</h2>\n<p>WebAuthn operates through two ceremonies: registration (creating a credential) and authentication (proving you own it). Both follow the same pattern. The server generates a challenge, the browser mediates the interaction with the authenticator, and the authenticator performs the cryptographic operation. Understanding these flows is essential before writing any integration code.</p>\n<h3 id=\"registration-with-navigatorcredentialscreate\" style=\"position:relative;\"><a href=\"#registration-with-navigatorcredentialscreate\" aria-label=\"registration with navigatorcredentialscreate permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Registration with <code class=\"language-text\">navigator.credentials.create()</code></h3>\n<p>Registration binds a new credential to a user account. The server initiates the process by generating a random challenge and specifying parameters about the relying party (your application) and the user.</p>\n<div\n              class=\"gatsby-code-button-container\"\n              data-toaster-id=\"30819271732343247000\"\n              data-toaster-class=\"gatsby-code-button-toaster\"\n              data-toaster-text-class=\"gatsby-code-button-toaster-text\"\n              data-toaster-text=\"Copied!\"\n              data-toaster-duration=\"3500\"\n              onClick=\"copyToClipboard(`const credential = await navigator.credentials.create({\n  publicKey: {\n    challenge: serverGeneratedChallenge,       // random bytes from your server\n    rp: { id: &quot;example.com&quot;, name: &quot;My App&quot; }, // relying party identity\n    user: {\n      id: userId,                              // unique user handle (ArrayBuffer)\n      name: &quot;user@example.com&quot;,\n      displayName: &quot;Jane Developer&quot;\n    },\n    pubKeyCredParams: [\n      { alg: -7, type: &quot;public-key&quot; },         // ES256 (recommended)\n      { alg: -257, type: &quot;public-key&quot; }        // RS256 (broad compatibility)\n    ],\n    authenticatorSelection: {\n      userVerification: &quot;preferred&quot;             // biometric/PIN if available\n    },\n    attestation: &quot;none&quot;                         // or &quot;direct&quot; for device verification\n    timeout: 60000\n  }\n});`, `30819271732343247000`)\"\n            >\n              <div\n                class=\"gatsby-code-button\"\n                data-tooltip=\"\"\n              >\n                <svg class=\"gatsby-code-button-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0V0z\"/><path d=\"M16 1H2v16h2V3h12V1zm-1 4l6 6v12H6V5h9zm-1 7h5.5L14 6.5V12z\"/></svg>\n              </div>\n            </div>\n<div class=\"gatsby-highlight\" data-language=\"javascript\"><pre class=\"language-javascript\"><code class=\"language-javascript\"><span class=\"token keyword\">const</span> credential <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> navigator<span class=\"token punctuation\">.</span>credentials<span class=\"token punctuation\">.</span><span class=\"token function\">create</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span>\n  <span class=\"token literal-property property\">publicKey</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token literal-property property\">challenge</span><span class=\"token operator\">:</span> serverGeneratedChallenge<span class=\"token punctuation\">,</span>       <span class=\"token comment\">// random bytes from your server</span>\n    <span class=\"token literal-property property\">rp</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span> <span class=\"token literal-property property\">id</span><span class=\"token operator\">:</span> <span class=\"token string\">\"example.com\"</span><span class=\"token punctuation\">,</span> <span class=\"token literal-property property\">name</span><span class=\"token operator\">:</span> <span class=\"token string\">\"My App\"</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span> <span class=\"token comment\">// relying party identity</span>\n    <span class=\"token literal-property property\">user</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span>\n      <span class=\"token literal-property property\">id</span><span class=\"token operator\">:</span> userId<span class=\"token punctuation\">,</span>                              <span class=\"token comment\">// unique user handle (ArrayBuffer)</span>\n      <span class=\"token literal-property property\">name</span><span class=\"token operator\">:</span> <span class=\"token string\">\"user@example.com\"</span><span class=\"token punctuation\">,</span>\n      <span class=\"token literal-property property\">displayName</span><span class=\"token operator\">:</span> <span class=\"token string\">\"Jane Developer\"</span>\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n    <span class=\"token literal-property property\">pubKeyCredParams</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">[</span>\n      <span class=\"token punctuation\">{</span> <span class=\"token literal-property property\">alg</span><span class=\"token operator\">:</span> <span class=\"token operator\">-</span><span class=\"token number\">7</span><span class=\"token punctuation\">,</span> <span class=\"token literal-property property\">type</span><span class=\"token operator\">:</span> <span class=\"token string\">\"public-key\"</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>         <span class=\"token comment\">// ES256 (recommended)</span>\n      <span class=\"token punctuation\">{</span> <span class=\"token literal-property property\">alg</span><span class=\"token operator\">:</span> <span class=\"token operator\">-</span><span class=\"token number\">257</span><span class=\"token punctuation\">,</span> <span class=\"token literal-property property\">type</span><span class=\"token operator\">:</span> <span class=\"token string\">\"public-key\"</span> <span class=\"token punctuation\">}</span>        <span class=\"token comment\">// RS256 (broad compatibility)</span>\n    <span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span>\n    <span class=\"token literal-property property\">authenticatorSelection</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span>\n      <span class=\"token literal-property property\">userVerification</span><span class=\"token operator\">:</span> <span class=\"token string\">\"preferred\"</span>             <span class=\"token comment\">// biometric/PIN if available</span>\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n    <span class=\"token literal-property property\">attestation</span><span class=\"token operator\">:</span> <span class=\"token string\">\"none\"</span>                         <span class=\"token comment\">// or \"direct\" for device verification</span>\n    <span class=\"token literal-property property\">timeout</span><span class=\"token operator\">:</span> <span class=\"token number\">60000</span>\n  <span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>When this call executes, the browser prompts the user to interact with an authenticator: a fingerprint sensor, a security key tap, or a platform prompt like Windows Hello. The authenticator generates a new key pair, locks the private key inside its secure storage, and returns the public key along with a credential ID. Your frontend sends this response to the server for verification and storage.</p>\n<h3 id=\"authentication-with-navigatorcredentialsget\" style=\"position:relative;\"><a href=\"#authentication-with-navigatorcredentialsget\" aria-label=\"authentication with navigatorcredentialsget permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Authentication with <code class=\"language-text\">navigator.credentials.get()</code></h3>\n<p>Authentication proves the user controls the private key registered earlier. The server sends a challenge and optionally a list of acceptable credential IDs.</p>\n<div\n              class=\"gatsby-code-button-container\"\n              data-toaster-id=\"44736837911205310000\"\n              data-toaster-class=\"gatsby-code-button-toaster\"\n              data-toaster-text-class=\"gatsby-code-button-toaster-text\"\n              data-toaster-text=\"Copied!\"\n              data-toaster-duration=\"3500\"\n              onClick=\"copyToClipboard(`const assertion = await navigator.credentials.get({\n  publicKey: {\n    challenge: serverGeneratedChallenge,\n    rpId: &quot;example.com&quot;,\n    allowCredentials: [{\n      id: storedCredentialId,                   // from registration\n      type: &quot;public-key&quot;,\n      transports: [&quot;usb&quot;, &quot;ble&quot;, &quot;internal&quot;]   // hint for authenticator discovery\n    }],\n    userVerification: &quot;preferred&quot;,\n    timeout: 60000\n  }\n});`, `44736837911205310000`)\"\n            >\n              <div\n                class=\"gatsby-code-button\"\n                data-tooltip=\"\"\n              >\n                <svg class=\"gatsby-code-button-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0V0z\"/><path d=\"M16 1H2v16h2V3h12V1zm-1 4l6 6v12H6V5h9zm-1 7h5.5L14 6.5V12z\"/></svg>\n              </div>\n            </div>\n<div class=\"gatsby-highlight\" data-language=\"javascript\"><pre class=\"language-javascript\"><code class=\"language-javascript\"><span class=\"token keyword\">const</span> assertion <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> navigator<span class=\"token punctuation\">.</span>credentials<span class=\"token punctuation\">.</span><span class=\"token function\">get</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span>\n  <span class=\"token literal-property property\">publicKey</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token literal-property property\">challenge</span><span class=\"token operator\">:</span> serverGeneratedChallenge<span class=\"token punctuation\">,</span>\n    <span class=\"token literal-property property\">rpId</span><span class=\"token operator\">:</span> <span class=\"token string\">\"example.com\"</span><span class=\"token punctuation\">,</span>\n    <span class=\"token literal-property property\">allowCredentials</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">[</span><span class=\"token punctuation\">{</span>\n      <span class=\"token literal-property property\">id</span><span class=\"token operator\">:</span> storedCredentialId<span class=\"token punctuation\">,</span>                   <span class=\"token comment\">// from registration</span>\n      <span class=\"token literal-property property\">type</span><span class=\"token operator\">:</span> <span class=\"token string\">\"public-key\"</span><span class=\"token punctuation\">,</span>\n      <span class=\"token literal-property property\">transports</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">[</span><span class=\"token string\">\"usb\"</span><span class=\"token punctuation\">,</span> <span class=\"token string\">\"ble\"</span><span class=\"token punctuation\">,</span> <span class=\"token string\">\"internal\"</span><span class=\"token punctuation\">]</span>   <span class=\"token comment\">// hint for authenticator discovery</span>\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span>\n    <span class=\"token literal-property property\">userVerification</span><span class=\"token operator\">:</span> <span class=\"token string\">\"preferred\"</span><span class=\"token punctuation\">,</span>\n    <span class=\"token literal-property property\">timeout</span><span class=\"token operator\">:</span> <span class=\"token number\">60000</span>\n  <span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>The authenticator locates the matching credential, verifies the user (biometric or PIN), and signs the challenge with the private key. The browser returns this signed assertion to your application, which forwards it to the server for signature verification.</p>\n<h3 id=\"the-backends-role\" style=\"position:relative;\"><a href=\"#the-backends-role\" aria-label=\"the backends role permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The Backend’s Role</h3>\n<p>The server has three responsibilities across both ceremonies.</p>\n<p>During registration, the backend must verify the attestation response. This means confirming that the challenge matches what was issued, the origin matches your application’s domain, and the public key uses an acceptable algorithm. Libraries like <code class=\"language-text\">@simplewebauthn/server</code> handle the cryptographic verification, but your application is responsible for storing the resulting credential data: the public key, credential ID, a signature counter, and the transports the authenticator supports.</p>\n<p>During authentication, the backend verifies the assertion signature against the stored public key. It also checks the signature counter. Authenticators increment this counter with each use, so a counter value lower than or equal to the stored value signals a potential cloned credential. After successful verification, the backend updates the stored counter and issues a session.</p>\n<p>The step-by-step flow looks like this:</p>\n<ol>\n<li>Server generates and stores a random challenge</li>\n<li>Frontend calls <code class=\"language-text\">navigator.credentials.create()</code> or <code class=\"language-text\">.get()</code> with the challenge</li>\n<li>Browser mediates the authenticator interaction, enforcing origin binding</li>\n<li>Authenticator performs the cryptographic operation after user verification</li>\n<li>Frontend sends the response to the server</li>\n<li>Server validates the response (challenge, origin, signature, counter)</li>\n<li>Server stores the credential (registration) or issues a session (authentication)</li>\n</ol>\n<p>Origin binding happens at step 3. The browser automatically includes the current origin in the data sent to the authenticator. If a user is on a phishing site at <code class=\"language-text\">evil-example.com</code>, the authenticator will not find a credential registered for that origin. The authentication fails silently, with no secret exposed to the attacker.</p>\n<h2 id=\"webauthn-spec-and-browser-support\" style=\"position:relative;\"><a href=\"#webauthn-spec-and-browser-support\" aria-label=\"webauthn spec and browser support permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>WebAuthn Spec and Browser Support</h2>\n<h3 id=\"the-w3c-specification\" style=\"position:relative;\"><a href=\"#the-w3c-specification\" aria-label=\"the w3c specification permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The W3C Specification</h3>\n<p>The WebAuthn standard has matured through three specification levels. <a href=\"https://www.w3.org/TR/webauthn-1/\" target=\"_blank\" rel=\"nofollow\">Level 1</a> became a W3C Recommendation in March 2019, establishing the core API for credential creation and assertion. <a href=\"https://www.w3.org/TR/webauthn-2/\" target=\"_blank\" rel=\"nofollow\">Level 2</a> followed in April 2021, adding support for cross-platform authenticator transports, improved attestation handling, and better alignment with the CTAP2 protocol. <a href=\"https://www.w3.org/TR/webauthn-3/\" target=\"_blank\" rel=\"nofollow\">Level 3</a> reached Candidate Recommendation status in January 2026 and introduces features like the PRF extension for deriving encryption keys from credentials, conditional mediation (the ability to autofill passkey prompts), and the Signal API for relying parties to communicate credential state changes back to authenticators.</p>\n<p>Each level builds on the previous one without breaking backward compatibility. If your application targets Level 2 features today, it will continue to work as Level 3 gains full browser adoption.</p>\n<h3 id=\"browser-compatibility-and-secure-contexts\" style=\"position:relative;\"><a href=\"#browser-compatibility-and-secure-contexts\" aria-label=\"browser compatibility and secure contexts permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Browser Compatibility and Secure Contexts</h3>\n<p>WebAuthn enjoys broad support across modern browsers. Chrome has supported it since version 67, Firefox since version 60, Edge since version 18, and Safari since version 13. On mobile, both Android (via Chrome and the platform credential manager) and iOS (via Safari with Face ID and Touch ID) provide full WebAuthn support. The practical result is that the vast majority of users on current browsers can use WebAuthn credentials without installing anything.</p>\n<p>One hard requirement: WebAuthn only works in <a href=\"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts\" target=\"_blank\" rel=\"nofollow\">secure contexts</a>. That means HTTPS in production, no exceptions. The API will not execute over plain HTTP. For local development, <code class=\"language-text\">localhost</code> is treated as a secure context by browsers, so you can test without configuring TLS certificates on your dev machine. This requirement exists because origin binding is the foundation of WebAuthn’s phishing resistance, and origin verification is only meaningful over authenticated connections.</p>\n<p>Browser support is not perfectly uniform across every feature. Platform authenticator availability depends on the operating system (Windows Hello, macOS Touch ID, Android biometrics), and newer Level 3 extensions like PRF are still rolling out across browsers and platforms. Implement feature detection using <code class=\"language-text\">PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()</code> and provide graceful fallbacks for users on older configurations.</p>\n<h3 id=\"from-u2f-to-webauthn\" style=\"position:relative;\"><a href=\"#from-u2f-to-webauthn\" aria-label=\"from u2f to webauthn permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>From U2F to WebAuthn</h3>\n<p>WebAuthn did not appear in a vacuum. It evolved from FIDO U2F (Universal 2nd Factor), a protocol the FIDO Alliance introduced in 2014 for hardware-based second-factor authentication. U2F was effective but limited: it only functioned as a second factor alongside a password, it required a physical USB security key, and it never saw native Safari support.</p>\n<p>WebAuthn addresses each of those limitations. It supports both multi-factor and single-factor (passwordless) authentication, so a fingerprint scan or security key tap can replace passwords entirely rather than just supplementing them. It works with platform authenticators built into devices, not only roaming USB keys. And it has universal browser adoption, something U2F never achieved.</p>\n<p>The transition is effectively complete. Chrome deprecated the U2F JavaScript API in version 98 and removed it entirely in version 115. Firefox deprecated U2F in favor of WebAuthn starting with version 60. Existing U2F security keys remain compatible with WebAuthn through CTAP1 backward compatibility, so users with older YubiKeys or Google Titan keys do not need to replace their hardware. For new implementations, WebAuthn through the FIDO2 framework is the only standard worth building on.</p>\n<h2 id=\"how-to-integrate-webauthn-in-your-auth-stack-frontend--backend\" style=\"position:relative;\"><a href=\"#how-to-integrate-webauthn-in-your-auth-stack-frontend--backend\" aria-label=\"how to integrate webauthn in your auth stack frontend  backend permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>How to Integrate WebAuthn in Your Auth Stack (Frontend + Backend)</h2>\n<p>The WebAuthn API surface is small, but the data flowing between client and server requires careful handling. The browser returns <code class=\"language-text\">ArrayBuffer</code> objects that need encoding before transmission. The server receives attestation and assertion structures that must be parsed, validated, and stored correctly. Here is a practical walkthrough of both sides.</p>\n<h3 id=\"frontend-registration\" style=\"position:relative;\"><a href=\"#frontend-registration\" aria-label=\"frontend registration permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Frontend: Registration</h3>\n<p>Registration starts with your server generating options (challenge, user info, relying party config) and ends with the browser returning a new credential.</p>\n<div\n              class=\"gatsby-code-button-container\"\n              data-toaster-id=\"32785373160216703000\"\n              data-toaster-class=\"gatsby-code-button-toaster\"\n              data-toaster-text-class=\"gatsby-code-button-toaster-text\"\n              data-toaster-text=\"Copied!\"\n              data-toaster-duration=\"3500\"\n              onClick=\"copyToClipboard(`async function registerCredential(userId: string): Promise<void> {\n  // 1. Fetch registration options from your server\n  const options = await fetch('/api/webauthn/register/options', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify({ userId })\n  }).then(res => res.json());\n\n  // 2. Call the WebAuthn API\n  const credential = await navigator.credentials.create({\n    publicKey: {\n      challenge: base64UrlToBuffer(options.challenge),\n      rp: { id: options.rpId, name: options.rpName },\n      user: {\n        id: base64UrlToBuffer(options.userId),\n        name: options.userName,\n        displayName: options.userDisplayName\n      },\n      pubKeyCredParams: [\n        { alg: -7, type: 'public-key' },   // ES256\n        { alg: -257, type: 'public-key' }  // RS256\n      ],\n      authenticatorSelection: {\n        userVerification: 'preferred',\n        residentKey: 'preferred'\n      },\n      attestation: 'none',\n      timeout: 60000\n    }\n  }) as PublicKeyCredential;\n\n  // 3. Send the response to the server for verification\n  const attestationResponse = credential.response as AuthenticatorAttestationResponse;\n  await fetch('/api/webauthn/register/verify', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify({\n      id: credential.id,\n      rawId: bufferToBase64Url(credential.rawId),\n      response: {\n        clientDataJSON: bufferToBase64Url(attestationResponse.clientDataJSON),\n        attestationObject: bufferToBase64Url(attestationResponse.attestationObject)\n      },\n      type: credential.type\n    })\n  });\n}`, `32785373160216703000`)\"\n            >\n              <div\n                class=\"gatsby-code-button\"\n                data-tooltip=\"\"\n              >\n                <svg class=\"gatsby-code-button-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0V0z\"/><path d=\"M16 1H2v16h2V3h12V1zm-1 4l6 6v12H6V5h9zm-1 7h5.5L14 6.5V12z\"/></svg>\n              </div>\n            </div>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">registerCredential</span><span class=\"token punctuation\">(</span>userId<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span><span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> <span class=\"token builtin\">Promise</span><span class=\"token operator\">&lt;</span><span class=\"token keyword\">void</span><span class=\"token operator\">></span> <span class=\"token punctuation\">{</span>\n  <span class=\"token comment\">// 1. Fetch registration options from your server</span>\n  <span class=\"token keyword\">const</span> options <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token function\">fetch</span><span class=\"token punctuation\">(</span><span class=\"token string\">'/api/webauthn/register/options'</span><span class=\"token punctuation\">,</span> <span class=\"token punctuation\">{</span>\n    method<span class=\"token operator\">:</span> <span class=\"token string\">'POST'</span><span class=\"token punctuation\">,</span>\n    headers<span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span> <span class=\"token string-property property\">'Content-Type'</span><span class=\"token operator\">:</span> <span class=\"token string\">'application/json'</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n    body<span class=\"token operator\">:</span> <span class=\"token constant\">JSON</span><span class=\"token punctuation\">.</span><span class=\"token function\">stringify</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span> userId <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">then</span><span class=\"token punctuation\">(</span>res <span class=\"token operator\">=></span> res<span class=\"token punctuation\">.</span><span class=\"token function\">json</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token comment\">// 2. Call the WebAuthn API</span>\n  <span class=\"token keyword\">const</span> credential <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> navigator<span class=\"token punctuation\">.</span>credentials<span class=\"token punctuation\">.</span><span class=\"token function\">create</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span>\n    publicKey<span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span>\n      challenge<span class=\"token operator\">:</span> <span class=\"token function\">base64UrlToBuffer</span><span class=\"token punctuation\">(</span>options<span class=\"token punctuation\">.</span>challenge<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n      rp<span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span> id<span class=\"token operator\">:</span> options<span class=\"token punctuation\">.</span>rpId<span class=\"token punctuation\">,</span> name<span class=\"token operator\">:</span> options<span class=\"token punctuation\">.</span>rpName <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n      user<span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span>\n        id<span class=\"token operator\">:</span> <span class=\"token function\">base64UrlToBuffer</span><span class=\"token punctuation\">(</span>options<span class=\"token punctuation\">.</span>userId<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n        name<span class=\"token operator\">:</span> options<span class=\"token punctuation\">.</span>userName<span class=\"token punctuation\">,</span>\n        displayName<span class=\"token operator\">:</span> options<span class=\"token punctuation\">.</span>userDisplayName\n      <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n      pubKeyCredParams<span class=\"token operator\">:</span> <span class=\"token punctuation\">[</span>\n        <span class=\"token punctuation\">{</span> alg<span class=\"token operator\">:</span> <span class=\"token operator\">-</span><span class=\"token number\">7</span><span class=\"token punctuation\">,</span> type<span class=\"token operator\">:</span> <span class=\"token string\">'public-key'</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>   <span class=\"token comment\">// ES256</span>\n        <span class=\"token punctuation\">{</span> alg<span class=\"token operator\">:</span> <span class=\"token operator\">-</span><span class=\"token number\">257</span><span class=\"token punctuation\">,</span> type<span class=\"token operator\">:</span> <span class=\"token string\">'public-key'</span> <span class=\"token punctuation\">}</span>  <span class=\"token comment\">// RS256</span>\n      <span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span>\n      authenticatorSelection<span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span>\n        userVerification<span class=\"token operator\">:</span> <span class=\"token string\">'preferred'</span><span class=\"token punctuation\">,</span>\n        residentKey<span class=\"token operator\">:</span> <span class=\"token string\">'preferred'</span>\n      <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n      attestation<span class=\"token operator\">:</span> <span class=\"token string\">'none'</span><span class=\"token punctuation\">,</span>\n      timeout<span class=\"token operator\">:</span> <span class=\"token number\">60000</span>\n    <span class=\"token punctuation\">}</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span> <span class=\"token keyword\">as</span> PublicKeyCredential<span class=\"token punctuation\">;</span>\n\n  <span class=\"token comment\">// 3. Send the response to the server for verification</span>\n  <span class=\"token keyword\">const</span> attestationResponse <span class=\"token operator\">=</span> credential<span class=\"token punctuation\">.</span>response <span class=\"token keyword\">as</span> AuthenticatorAttestationResponse<span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">await</span> <span class=\"token function\">fetch</span><span class=\"token punctuation\">(</span><span class=\"token string\">'/api/webauthn/register/verify'</span><span class=\"token punctuation\">,</span> <span class=\"token punctuation\">{</span>\n    method<span class=\"token operator\">:</span> <span class=\"token string\">'POST'</span><span class=\"token punctuation\">,</span>\n    headers<span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span> <span class=\"token string-property property\">'Content-Type'</span><span class=\"token operator\">:</span> <span class=\"token string\">'application/json'</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n    body<span class=\"token operator\">:</span> <span class=\"token constant\">JSON</span><span class=\"token punctuation\">.</span><span class=\"token function\">stringify</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span>\n      id<span class=\"token operator\">:</span> credential<span class=\"token punctuation\">.</span>id<span class=\"token punctuation\">,</span>\n      rawId<span class=\"token operator\">:</span> <span class=\"token function\">bufferToBase64Url</span><span class=\"token punctuation\">(</span>credential<span class=\"token punctuation\">.</span>rawId<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n      response<span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span>\n        clientDataJSON<span class=\"token operator\">:</span> <span class=\"token function\">bufferToBase64Url</span><span class=\"token punctuation\">(</span>attestationResponse<span class=\"token punctuation\">.</span>clientDataJSON<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n        attestationObject<span class=\"token operator\">:</span> <span class=\"token function\">bufferToBase64Url</span><span class=\"token punctuation\">(</span>attestationResponse<span class=\"token punctuation\">.</span>attestationObject<span class=\"token punctuation\">)</span>\n      <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n      type<span class=\"token operator\">:</span> credential<span class=\"token punctuation\">.</span>type\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<h3 id=\"frontend-authentication\" style=\"position:relative;\"><a href=\"#frontend-authentication\" aria-label=\"frontend authentication permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Frontend: Authentication</h3>\n<p>Authentication follows the same pattern. The server provides a challenge and optionally a list of allowed credential IDs, the browser collects the signed assertion, and the frontend forwards it.</p>\n<div\n              class=\"gatsby-code-button-container\"\n              data-toaster-id=\"5673374977511814000\"\n              data-toaster-class=\"gatsby-code-button-toaster\"\n              data-toaster-text-class=\"gatsby-code-button-toaster-text\"\n              data-toaster-text=\"Copied!\"\n              data-toaster-duration=\"3500\"\n              onClick=\"copyToClipboard(`async function authenticate(): Promise<void> {\n  const options = await fetch('/api/webauthn/login/options', {\n    method: 'POST'\n  }).then(res => res.json());\n\n  const assertion = await navigator.credentials.get({\n    publicKey: {\n      challenge: base64UrlToBuffer(options.challenge),\n      rpId: options.rpId,\n      allowCredentials: options.allowCredentials?.map((cred: any) => ({\n        id: base64UrlToBuffer(cred.id),\n        type: 'public-key',\n        transports: cred.transports\n      })),\n      userVerification: 'preferred',\n      timeout: 60000\n    }\n  }) as PublicKeyCredential;\n\n  const assertionResponse = assertion.response as AuthenticatorAssertionResponse;\n  await fetch('/api/webauthn/login/verify', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify({\n      id: assertion.id,\n      rawId: bufferToBase64Url(assertion.rawId),\n      response: {\n        clientDataJSON: bufferToBase64Url(assertionResponse.clientDataJSON),\n        authenticatorData: bufferToBase64Url(assertionResponse.authenticatorData),\n        signature: bufferToBase64Url(assertionResponse.signature),\n        userHandle: assertionResponse.userHandle\n          ? bufferToBase64Url(assertionResponse.userHandle)\n          : null\n      },\n      type: assertion.type\n    })\n  });\n}`, `5673374977511814000`)\"\n            >\n              <div\n                class=\"gatsby-code-button\"\n                data-tooltip=\"\"\n              >\n                <svg class=\"gatsby-code-button-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0V0z\"/><path d=\"M16 1H2v16h2V3h12V1zm-1 4l6 6v12H6V5h9zm-1 7h5.5L14 6.5V12z\"/></svg>\n              </div>\n            </div>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">authenticate</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> <span class=\"token builtin\">Promise</span><span class=\"token operator\">&lt;</span><span class=\"token keyword\">void</span><span class=\"token operator\">></span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> options <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token function\">fetch</span><span class=\"token punctuation\">(</span><span class=\"token string\">'/api/webauthn/login/options'</span><span class=\"token punctuation\">,</span> <span class=\"token punctuation\">{</span>\n    method<span class=\"token operator\">:</span> <span class=\"token string\">'POST'</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">then</span><span class=\"token punctuation\">(</span>res <span class=\"token operator\">=></span> res<span class=\"token punctuation\">.</span><span class=\"token function\">json</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token keyword\">const</span> assertion <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> navigator<span class=\"token punctuation\">.</span>credentials<span class=\"token punctuation\">.</span><span class=\"token function\">get</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span>\n    publicKey<span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span>\n      challenge<span class=\"token operator\">:</span> <span class=\"token function\">base64UrlToBuffer</span><span class=\"token punctuation\">(</span>options<span class=\"token punctuation\">.</span>challenge<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n      rpId<span class=\"token operator\">:</span> options<span class=\"token punctuation\">.</span>rpId<span class=\"token punctuation\">,</span>\n      allowCredentials<span class=\"token operator\">:</span> options<span class=\"token punctuation\">.</span>allowCredentials<span class=\"token operator\">?.</span><span class=\"token function\">map</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">(</span>cred<span class=\"token operator\">:</span> <span class=\"token builtin\">any</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span>\n        id<span class=\"token operator\">:</span> <span class=\"token function\">base64UrlToBuffer</span><span class=\"token punctuation\">(</span>cred<span class=\"token punctuation\">.</span>id<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n        type<span class=\"token operator\">:</span> <span class=\"token string\">'public-key'</span><span class=\"token punctuation\">,</span>\n        transports<span class=\"token operator\">:</span> cred<span class=\"token punctuation\">.</span>transports\n      <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n      userVerification<span class=\"token operator\">:</span> <span class=\"token string\">'preferred'</span><span class=\"token punctuation\">,</span>\n      timeout<span class=\"token operator\">:</span> <span class=\"token number\">60000</span>\n    <span class=\"token punctuation\">}</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span> <span class=\"token keyword\">as</span> PublicKeyCredential<span class=\"token punctuation\">;</span>\n\n  <span class=\"token keyword\">const</span> assertionResponse <span class=\"token operator\">=</span> assertion<span class=\"token punctuation\">.</span>response <span class=\"token keyword\">as</span> AuthenticatorAssertionResponse<span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">await</span> <span class=\"token function\">fetch</span><span class=\"token punctuation\">(</span><span class=\"token string\">'/api/webauthn/login/verify'</span><span class=\"token punctuation\">,</span> <span class=\"token punctuation\">{</span>\n    method<span class=\"token operator\">:</span> <span class=\"token string\">'POST'</span><span class=\"token punctuation\">,</span>\n    headers<span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span> <span class=\"token string-property property\">'Content-Type'</span><span class=\"token operator\">:</span> <span class=\"token string\">'application/json'</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n    body<span class=\"token operator\">:</span> <span class=\"token constant\">JSON</span><span class=\"token punctuation\">.</span><span class=\"token function\">stringify</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span>\n      id<span class=\"token operator\">:</span> assertion<span class=\"token punctuation\">.</span>id<span class=\"token punctuation\">,</span>\n      rawId<span class=\"token operator\">:</span> <span class=\"token function\">bufferToBase64Url</span><span class=\"token punctuation\">(</span>assertion<span class=\"token punctuation\">.</span>rawId<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n      response<span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span>\n        clientDataJSON<span class=\"token operator\">:</span> <span class=\"token function\">bufferToBase64Url</span><span class=\"token punctuation\">(</span>assertionResponse<span class=\"token punctuation\">.</span>clientDataJSON<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n        authenticatorData<span class=\"token operator\">:</span> <span class=\"token function\">bufferToBase64Url</span><span class=\"token punctuation\">(</span>assertionResponse<span class=\"token punctuation\">.</span>authenticatorData<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n        signature<span class=\"token operator\">:</span> <span class=\"token function\">bufferToBase64Url</span><span class=\"token punctuation\">(</span>assertionResponse<span class=\"token punctuation\">.</span>signature<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n        userHandle<span class=\"token operator\">:</span> assertionResponse<span class=\"token punctuation\">.</span>userHandle\n          <span class=\"token operator\">?</span> <span class=\"token function\">bufferToBase64Url</span><span class=\"token punctuation\">(</span>assertionResponse<span class=\"token punctuation\">.</span>userHandle<span class=\"token punctuation\">)</span>\n          <span class=\"token operator\">:</span> <span class=\"token keyword\">null</span>\n      <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n      type<span class=\"token operator\">:</span> assertion<span class=\"token punctuation\">.</span>type\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>Both flows depend on a pair of encoding helpers. The WebAuthn API works with <code class=\"language-text\">ArrayBuffer</code>, but JSON transport requires base64url strings.</p>\n<div\n              class=\"gatsby-code-button-container\"\n              data-toaster-id=\"51170931737474870000\"\n              data-toaster-class=\"gatsby-code-button-toaster\"\n              data-toaster-text-class=\"gatsby-code-button-toaster-text\"\n              data-toaster-text=\"Copied!\"\n              data-toaster-duration=\"3500\"\n              onClick=\"copyToClipboard(`function bufferToBase64Url(buffer: ArrayBuffer): string {\n  return btoa(String.fromCharCode(...new Uint8Array(buffer)))\n    .replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+\\$/, '');\n}\n\nfunction base64UrlToBuffer(base64url: string): ArrayBuffer {\n  const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');\n  const binary = atob(base64);\n  return Uint8Array.from(binary, c => c.charCodeAt(0)).buffer;\n}`, `51170931737474870000`)\"\n            >\n              <div\n                class=\"gatsby-code-button\"\n                data-tooltip=\"\"\n              >\n                <svg class=\"gatsby-code-button-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0V0z\"/><path d=\"M16 1H2v16h2V3h12V1zm-1 4l6 6v12H6V5h9zm-1 7h5.5L14 6.5V12z\"/></svg>\n              </div>\n            </div>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token keyword\">function</span> <span class=\"token function\">bufferToBase64Url</span><span class=\"token punctuation\">(</span>buffer<span class=\"token operator\">:</span> ArrayBuffer<span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> <span class=\"token builtin\">string</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">return</span> <span class=\"token function\">btoa</span><span class=\"token punctuation\">(</span>String<span class=\"token punctuation\">.</span><span class=\"token function\">fromCharCode</span><span class=\"token punctuation\">(</span><span class=\"token operator\">...</span><span class=\"token keyword\">new</span> <span class=\"token class-name\">Uint8Array</span><span class=\"token punctuation\">(</span>buffer<span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span>\n    <span class=\"token punctuation\">.</span><span class=\"token function\">replace</span><span class=\"token punctuation\">(</span><span class=\"token regex\"><span class=\"token regex-delimiter\">/</span><span class=\"token regex-source language-regex\">\\+</span><span class=\"token regex-delimiter\">/</span><span class=\"token regex-flags\">g</span></span><span class=\"token punctuation\">,</span> <span class=\"token string\">'-'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">replace</span><span class=\"token punctuation\">(</span><span class=\"token regex\"><span class=\"token regex-delimiter\">/</span><span class=\"token regex-source language-regex\">\\/</span><span class=\"token regex-delimiter\">/</span><span class=\"token regex-flags\">g</span></span><span class=\"token punctuation\">,</span> <span class=\"token string\">'_'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">replace</span><span class=\"token punctuation\">(</span><span class=\"token regex\"><span class=\"token regex-delimiter\">/</span><span class=\"token regex-source language-regex\">=+$</span><span class=\"token regex-delimiter\">/</span></span><span class=\"token punctuation\">,</span> <span class=\"token string\">''</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">function</span> <span class=\"token function\">base64UrlToBuffer</span><span class=\"token punctuation\">(</span>base64url<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span><span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> ArrayBuffer <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> base64 <span class=\"token operator\">=</span> base64url<span class=\"token punctuation\">.</span><span class=\"token function\">replace</span><span class=\"token punctuation\">(</span><span class=\"token regex\"><span class=\"token regex-delimiter\">/</span><span class=\"token regex-source language-regex\">-</span><span class=\"token regex-delimiter\">/</span><span class=\"token regex-flags\">g</span></span><span class=\"token punctuation\">,</span> <span class=\"token string\">'+'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">replace</span><span class=\"token punctuation\">(</span><span class=\"token regex\"><span class=\"token regex-delimiter\">/</span><span class=\"token regex-source language-regex\">_</span><span class=\"token regex-delimiter\">/</span><span class=\"token regex-flags\">g</span></span><span class=\"token punctuation\">,</span> <span class=\"token string\">'/'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">const</span> binary <span class=\"token operator\">=</span> <span class=\"token function\">atob</span><span class=\"token punctuation\">(</span>base64<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">return</span> Uint8Array<span class=\"token punctuation\">.</span><span class=\"token function\">from</span><span class=\"token punctuation\">(</span>binary<span class=\"token punctuation\">,</span> c <span class=\"token operator\">=></span> c<span class=\"token punctuation\">.</span><span class=\"token function\">charCodeAt</span><span class=\"token punctuation\">(</span><span class=\"token number\">0</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span>buffer<span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<h3 id=\"backend-parsing-and-verification\" style=\"position:relative;\"><a href=\"#backend-parsing-and-verification\" aria-label=\"backend parsing and verification permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Backend: Parsing and Verification</h3>\n<p>The server’s job is to unpack the response, verify it cryptographically, and reject anything that does not match. For registration, that means parsing <code class=\"language-text\">clientDataJSON</code> to confirm the challenge and origin, then extracting the public key from the <code class=\"language-text\">attestationObject</code>. For authentication, it means validating the signature over the <code class=\"language-text\">authenticatorData</code> and <code class=\"language-text\">clientDataJSON</code> hash against the stored public key.</p>\n<p>Libraries like <a href=\"https://github.com/MasterKale/SimpleWebAuthn\" target=\"_blank\" rel=\"nofollow\"><code class=\"language-text\">@simplewebauthn/server</code></a> handle the low-level CBOR decoding and signature math. Using one is strongly recommended over rolling your own verification.</p>\n<div\n              class=\"gatsby-code-button-container\"\n              data-toaster-id=\"81793658301006120000\"\n              data-toaster-class=\"gatsby-code-button-toaster\"\n              data-toaster-text-class=\"gatsby-code-button-toaster-text\"\n              data-toaster-text=\"Copied!\"\n              data-toaster-duration=\"3500\"\n              onClick=\"copyToClipboard(`const {\n  generateRegistrationOptions,\n  verifyRegistrationResponse,\n  generateAuthenticationOptions,\n  verifyAuthenticationResponse\n} = require('@simplewebauthn/server');\n\nconst RP_ID = 'example.com';\nconst ORIGIN = 'https://example.com';\n\n// Registration verification\nasync function handleRegistrationVerify(req, res) {\n  const { body } = req;\n  const expectedChallenge = await getStoredChallenge(req.session.userId);\n\n  const verification = await verifyRegistrationResponse({\n    response: body,\n    expectedChallenge,\n    expectedOrigin: ORIGIN,\n    expectedRPID: RP_ID\n  });\n\n  if (verification.verified) {\n    const { credential } = verification.registrationInfo;\n    // Store the credential (see schema below)\n    await saveCredential(req.session.userId, {\n      credentialId: credential.id,\n      publicKey: Buffer.from(credential.publicKey),\n      counter: credential.counter,\n      transports: body.response.transports || []\n    });\n  }\n}\n\n// Authentication verification\nasync function handleAuthVerify(req, res) {\n  const { body } = req;\n  const expectedChallenge = await getStoredChallenge(body.userId);\n  const storedCredential = await getCredential(body.id);\n\n  const verification = await verifyAuthenticationResponse({\n    response: body,\n    expectedChallenge,\n    expectedOrigin: ORIGIN,\n    expectedRPID: RP_ID,\n    credential: storedCredential\n  });\n\n  if (verification.verified) {\n    // Update counter to detect cloned authenticators\n    await updateCredentialCounter(\n      body.id,\n      verification.authenticationInfo.newCounter\n    );\n    // Issue session\n  }\n}`, `81793658301006120000`)\"\n            >\n              <div\n                class=\"gatsby-code-button\"\n                data-tooltip=\"\"\n              >\n                <svg class=\"gatsby-code-button-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0V0z\"/><path d=\"M16 1H2v16h2V3h12V1zm-1 4l6 6v12H6V5h9zm-1 7h5.5L14 6.5V12z\"/></svg>\n              </div>\n            </div>\n<div class=\"gatsby-highlight\" data-language=\"javascript\"><pre class=\"language-javascript\"><code class=\"language-javascript\"><span class=\"token keyword\">const</span> <span class=\"token punctuation\">{</span>\n  generateRegistrationOptions<span class=\"token punctuation\">,</span>\n  verifyRegistrationResponse<span class=\"token punctuation\">,</span>\n  generateAuthenticationOptions<span class=\"token punctuation\">,</span>\n  verifyAuthenticationResponse\n<span class=\"token punctuation\">}</span> <span class=\"token operator\">=</span> <span class=\"token function\">require</span><span class=\"token punctuation\">(</span><span class=\"token string\">'@simplewebauthn/server'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">const</span> <span class=\"token constant\">RP_ID</span> <span class=\"token operator\">=</span> <span class=\"token string\">'example.com'</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">const</span> <span class=\"token constant\">ORIGIN</span> <span class=\"token operator\">=</span> <span class=\"token string\">'https://example.com'</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token comment\">// Registration verification</span>\n<span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">handleRegistrationVerify</span><span class=\"token punctuation\">(</span><span class=\"token parameter\">req<span class=\"token punctuation\">,</span> res</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> <span class=\"token punctuation\">{</span> body <span class=\"token punctuation\">}</span> <span class=\"token operator\">=</span> req<span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">const</span> expectedChallenge <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token function\">getStoredChallenge</span><span class=\"token punctuation\">(</span>req<span class=\"token punctuation\">.</span>session<span class=\"token punctuation\">.</span>userId<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token keyword\">const</span> verification <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token function\">verifyRegistrationResponse</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span>\n    <span class=\"token literal-property property\">response</span><span class=\"token operator\">:</span> body<span class=\"token punctuation\">,</span>\n    expectedChallenge<span class=\"token punctuation\">,</span>\n    <span class=\"token literal-property property\">expectedOrigin</span><span class=\"token operator\">:</span> <span class=\"token constant\">ORIGIN</span><span class=\"token punctuation\">,</span>\n    <span class=\"token literal-property property\">expectedRPID</span><span class=\"token operator\">:</span> <span class=\"token constant\">RP_ID</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>verification<span class=\"token punctuation\">.</span>verified<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">const</span> <span class=\"token punctuation\">{</span> credential <span class=\"token punctuation\">}</span> <span class=\"token operator\">=</span> verification<span class=\"token punctuation\">.</span>registrationInfo<span class=\"token punctuation\">;</span>\n    <span class=\"token comment\">// Store the credential (see schema below)</span>\n    <span class=\"token keyword\">await</span> <span class=\"token function\">saveCredential</span><span class=\"token punctuation\">(</span>req<span class=\"token punctuation\">.</span>session<span class=\"token punctuation\">.</span>userId<span class=\"token punctuation\">,</span> <span class=\"token punctuation\">{</span>\n      <span class=\"token literal-property property\">credentialId</span><span class=\"token operator\">:</span> credential<span class=\"token punctuation\">.</span>id<span class=\"token punctuation\">,</span>\n      <span class=\"token literal-property property\">publicKey</span><span class=\"token operator\">:</span> Buffer<span class=\"token punctuation\">.</span><span class=\"token function\">from</span><span class=\"token punctuation\">(</span>credential<span class=\"token punctuation\">.</span>publicKey<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n      <span class=\"token literal-property property\">counter</span><span class=\"token operator\">:</span> credential<span class=\"token punctuation\">.</span>counter<span class=\"token punctuation\">,</span>\n      <span class=\"token literal-property property\">transports</span><span class=\"token operator\">:</span> body<span class=\"token punctuation\">.</span>response<span class=\"token punctuation\">.</span>transports <span class=\"token operator\">||</span> <span class=\"token punctuation\">[</span><span class=\"token punctuation\">]</span>\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token comment\">// Authentication verification</span>\n<span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">handleAuthVerify</span><span class=\"token punctuation\">(</span><span class=\"token parameter\">req<span class=\"token punctuation\">,</span> res</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> <span class=\"token punctuation\">{</span> body <span class=\"token punctuation\">}</span> <span class=\"token operator\">=</span> req<span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">const</span> expectedChallenge <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token function\">getStoredChallenge</span><span class=\"token punctuation\">(</span>body<span class=\"token punctuation\">.</span>userId<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">const</span> storedCredential <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token function\">getCredential</span><span class=\"token punctuation\">(</span>body<span class=\"token punctuation\">.</span>id<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token keyword\">const</span> verification <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token function\">verifyAuthenticationResponse</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span>\n    <span class=\"token literal-property property\">response</span><span class=\"token operator\">:</span> body<span class=\"token punctuation\">,</span>\n    expectedChallenge<span class=\"token punctuation\">,</span>\n    <span class=\"token literal-property property\">expectedOrigin</span><span class=\"token operator\">:</span> <span class=\"token constant\">ORIGIN</span><span class=\"token punctuation\">,</span>\n    <span class=\"token literal-property property\">expectedRPID</span><span class=\"token operator\">:</span> <span class=\"token constant\">RP_ID</span><span class=\"token punctuation\">,</span>\n    <span class=\"token literal-property property\">credential</span><span class=\"token operator\">:</span> storedCredential\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>verification<span class=\"token punctuation\">.</span>verified<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token comment\">// Update counter to detect cloned authenticators</span>\n    <span class=\"token keyword\">await</span> <span class=\"token function\">updateCredentialCounter</span><span class=\"token punctuation\">(</span>\n      body<span class=\"token punctuation\">.</span>id<span class=\"token punctuation\">,</span>\n      verification<span class=\"token punctuation\">.</span>authenticationInfo<span class=\"token punctuation\">.</span>newCounter\n    <span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n    <span class=\"token comment\">// Issue session</span>\n  <span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>Three fields inside <code class=\"language-text\">clientDataJSON</code> matter most during verification: <code class=\"language-text\">type</code> (must be <code class=\"language-text\">webauthn.create</code> for registration or <code class=\"language-text\">webauthn.get</code> for authentication), <code class=\"language-text\">challenge</code> (must match the server-issued value), and <code class=\"language-text\">origin</code> (must match your relying party origin exactly). If any of these fail, reject the response. Never trust <code class=\"language-text\">req.headers.origin</code> as your expected origin; hardcode it or load it from your server configuration.</p>\n<h3 id=\"credential-storage\" style=\"position:relative;\"><a href=\"#credential-storage\" aria-label=\"credential storage permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Credential Storage</h3>\n<p>Each user can register multiple credentials (a laptop fingerprint sensor, a phone, a backup security key), so your storage needs to support a one-to-many relationship between users and credentials.</p>\n<div\n              class=\"gatsby-code-button-container\"\n              data-toaster-id=\"47080506901997880000\"\n              data-toaster-class=\"gatsby-code-button-toaster\"\n              data-toaster-text-class=\"gatsby-code-button-toaster-text\"\n              data-toaster-text=\"Copied!\"\n              data-toaster-duration=\"3500\"\n              onClick=\"copyToClipboard(`CREATE TABLE webauthn_credentials (\n  credential_id  TEXT PRIMARY KEY,\n  user_id        TEXT NOT NULL REFERENCES users(id),\n  public_key     BYTEA NOT NULL,\n  counter        BIGINT NOT NULL DEFAULT 0,\n  transports     TEXT[],           -- e.g. {'internal', 'usb'}\n  created_at     TIMESTAMP DEFAULT NOW(),\n  last_used_at   TIMESTAMP,\n  friendly_name  TEXT              -- &quot;MacBook Touch ID&quot;, &quot;Backup YubiKey&quot;\n);`, `47080506901997880000`)\"\n            >\n              <div\n                class=\"gatsby-code-button\"\n                data-tooltip=\"\"\n              >\n                <svg class=\"gatsby-code-button-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0V0z\"/><path d=\"M16 1H2v16h2V3h12V1zm-1 4l6 6v12H6V5h9zm-1 7h5.5L14 6.5V12z\"/></svg>\n              </div>\n            </div>\n<div class=\"gatsby-highlight\" data-language=\"sql\"><pre class=\"language-sql\"><code class=\"language-sql\"><span class=\"token keyword\">CREATE</span> <span class=\"token keyword\">TABLE</span> webauthn_credentials <span class=\"token punctuation\">(</span>\n  credential_id  <span class=\"token keyword\">TEXT</span> <span class=\"token keyword\">PRIMARY</span> <span class=\"token keyword\">KEY</span><span class=\"token punctuation\">,</span>\n  user_id        <span class=\"token keyword\">TEXT</span> <span class=\"token operator\">NOT</span> <span class=\"token boolean\">NULL</span> <span class=\"token keyword\">REFERENCES</span> users<span class=\"token punctuation\">(</span>id<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n  public_key     BYTEA <span class=\"token operator\">NOT</span> <span class=\"token boolean\">NULL</span><span class=\"token punctuation\">,</span>\n  counter        <span class=\"token keyword\">BIGINT</span> <span class=\"token operator\">NOT</span> <span class=\"token boolean\">NULL</span> <span class=\"token keyword\">DEFAULT</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span>\n  transports     <span class=\"token keyword\">TEXT</span><span class=\"token punctuation\">[</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span>           <span class=\"token comment\">-- e.g. {'internal', 'usb'}</span>\n  created_at     <span class=\"token keyword\">TIMESTAMP</span> <span class=\"token keyword\">DEFAULT</span> <span class=\"token function\">NOW</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n  last_used_at   <span class=\"token keyword\">TIMESTAMP</span><span class=\"token punctuation\">,</span>\n  friendly_name  <span class=\"token keyword\">TEXT</span>              <span class=\"token comment\">-- \"MacBook Touch ID\", \"Backup YubiKey\"</span>\n<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>Track <code class=\"language-text\">last_used_at</code> so users can identify stale credentials in their account settings. Store <code class=\"language-text\">transports</code> so the browser can hint which authenticator type to prompt during login. Include a <code class=\"language-text\">friendly_name</code> column so users can distinguish between their registered devices. The <code class=\"language-text\">counter</code> column is your cloned-authenticator detection mechanism: if an authentication response arrives with a counter value less than or equal to the stored value, flag the credential and force re-registration.</p>\n<h2 id=\"webauthn--fallback-and-progressive-enhancement-strategies\" style=\"position:relative;\"><a href=\"#webauthn--fallback-and-progressive-enhancement-strategies\" aria-label=\"webauthn  fallback and progressive enhancement strategies permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>WebAuthn + Fallback and Progressive Enhancement Strategies</h2>\n<p>WebAuthn is the strongest authentication mechanism available in browsers today, but you cannot assume every user has access to it. Some users are on older devices without biometric sensors. Some are logging in from a borrowed machine. Some work in environments where USB security keys are impractical. A production auth flow needs to handle all of these cases without degrading security for the users who <em>can</em> use WebAuthn.</p>\n<h3 id=\"conditional-offering-detect-before-you-prompt\" style=\"position:relative;\"><a href=\"#conditional-offering-detect-before-you-prompt\" aria-label=\"conditional offering detect before you prompt permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Conditional Offering: Detect Before You Prompt</h3>\n<p>The right approach is progressive enhancement: offer WebAuthn when the device supports it, and fall back gracefully when it does not. The WebAuthn API provides the tools for this through feature detection.</p>\n<div\n              class=\"gatsby-code-button-container\"\n              data-toaster-id=\"82887643474452220000\"\n              data-toaster-class=\"gatsby-code-button-toaster\"\n              data-toaster-text-class=\"gatsby-code-button-toaster-text\"\n              data-toaster-text=\"Copied!\"\n              data-toaster-duration=\"3500\"\n              onClick=\"copyToClipboard(`async function getAvailableAuthMethods() {\n  const methods = ['password']; // baseline always available\n\n  if (window.PublicKeyCredential) {\n    methods.push('webauthn-roaming'); // security key support\n\n    const platformAvailable =\n      await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();\n\n    if (platformAvailable) {\n      methods.push('webauthn-platform'); // biometric/PIN support\n    }\n  }\n\n  return methods;\n}`, `82887643474452220000`)\"\n            >\n              <div\n                class=\"gatsby-code-button\"\n                data-tooltip=\"\"\n              >\n                <svg class=\"gatsby-code-button-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0V0z\"/><path d=\"M16 1H2v16h2V3h12V1zm-1 4l6 6v12H6V5h9zm-1 7h5.5L14 6.5V12z\"/></svg>\n              </div>\n            </div>\n<div class=\"gatsby-highlight\" data-language=\"javascript\"><pre class=\"language-javascript\"><code class=\"language-javascript\"><span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">getAvailableAuthMethods</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> methods <span class=\"token operator\">=</span> <span class=\"token punctuation\">[</span><span class=\"token string\">'password'</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">;</span> <span class=\"token comment\">// baseline always available</span>\n\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>window<span class=\"token punctuation\">.</span>PublicKeyCredential<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    methods<span class=\"token punctuation\">.</span><span class=\"token function\">push</span><span class=\"token punctuation\">(</span><span class=\"token string\">'webauthn-roaming'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span> <span class=\"token comment\">// security key support</span>\n\n    <span class=\"token keyword\">const</span> platformAvailable <span class=\"token operator\">=</span>\n      <span class=\"token keyword\">await</span> PublicKeyCredential<span class=\"token punctuation\">.</span><span class=\"token function\">isUserVerifyingPlatformAuthenticatorAvailable</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n    <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>platformAvailable<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n      methods<span class=\"token punctuation\">.</span><span class=\"token function\">push</span><span class=\"token punctuation\">(</span><span class=\"token string\">'webauthn-platform'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span> <span class=\"token comment\">// biometric/PIN support</span>\n    <span class=\"token punctuation\">}</span>\n  <span class=\"token punctuation\">}</span>\n\n  <span class=\"token keyword\">return</span> methods<span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p><code class=\"language-text\">PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()</code> tells you whether the device has a built-in authenticator like Touch ID, Face ID, or Windows Hello. If it returns <code class=\"language-text\">false</code>, prompting for a platform authenticator will only confuse the user. Check first, then adapt the UI accordingly.</p>\n<p>Conditional mediation, introduced in WebAuthn Level 3, takes this further. With <code class=\"language-text\">navigator.credentials.get({ mediation: \"conditional\" })</code>, the browser can surface passkey options in the autofill dropdown alongside saved passwords. Users who have registered a passkey see it as a login option without any extra UI. Users who have not registered one see the standard password field. No special prompts, no feature flags on your end.</p>\n<h3 id=\"fallback-flows\" style=\"position:relative;\"><a href=\"#fallback-flows\" aria-label=\"fallback flows permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Fallback Flows</h3>\n<p>When WebAuthn is unavailable or fails (the user cancels the prompt, a timeout occurs, the authenticator is not recognized), your application needs a clear fallback hierarchy. The goal is to maintain security while keeping the user moving forward.</p>\n<p>A practical fallback chain looks like this:</p>\n<ol>\n<li>WebAuthn (platform or roaming authenticator)</li>\n<li>TOTP via authenticator app</li>\n<li>One-time code via email or SMS</li>\n<li>Pre-generated recovery codes</li>\n<li>Administrative account recovery with identity verification</li>\n</ol>\n<div\n              class=\"gatsby-code-button-container\"\n              data-toaster-id=\"39766619050844060000\"\n              data-toaster-class=\"gatsby-code-button-toaster\"\n              data-toaster-text-class=\"gatsby-code-button-toaster-text\"\n              data-toaster-text=\"Copied!\"\n              data-toaster-duration=\"3500\"\n              onClick=\"copyToClipboard(`async function handleAuthFallback(userId, failedMethod) {\n  const registeredMethods = await getUserMFAMethods(userId);\n\n  if (failedMethod === 'webauthn' && registeredMethods.totp) {\n    return { fallback: 'totp', message: 'Enter the code from your authenticator app.' };\n  }\n\n  if (registeredMethods.backupCodes > 0) {\n    return { fallback: 'backup', message: 'Enter one of your recovery codes.' };\n  }\n\n  return { fallback: 'recovery', message: 'Contact support to verify your identity.' };\n}`, `39766619050844060000`)\"\n            >\n              <div\n                class=\"gatsby-code-button\"\n                data-tooltip=\"\"\n              >\n                <svg class=\"gatsby-code-button-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0V0z\"/><path d=\"M16 1H2v16h2V3h12V1zm-1 4l6 6v12H6V5h9zm-1 7h5.5L14 6.5V12z\"/></svg>\n              </div>\n            </div>\n<div class=\"gatsby-highlight\" data-language=\"javascript\"><pre class=\"language-javascript\"><code class=\"language-javascript\"><span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">handleAuthFallback</span><span class=\"token punctuation\">(</span><span class=\"token parameter\">userId<span class=\"token punctuation\">,</span> failedMethod</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> registeredMethods <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token function\">getUserMFAMethods</span><span class=\"token punctuation\">(</span>userId<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>failedMethod <span class=\"token operator\">===</span> <span class=\"token string\">'webauthn'</span> <span class=\"token operator\">&amp;&amp;</span> registeredMethods<span class=\"token punctuation\">.</span>totp<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span> <span class=\"token literal-property property\">fallback</span><span class=\"token operator\">:</span> <span class=\"token string\">'totp'</span><span class=\"token punctuation\">,</span> <span class=\"token literal-property property\">message</span><span class=\"token operator\">:</span> <span class=\"token string\">'Enter the code from your authenticator app.'</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>registeredMethods<span class=\"token punctuation\">.</span>backupCodes <span class=\"token operator\">></span> <span class=\"token number\">0</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span> <span class=\"token literal-property property\">fallback</span><span class=\"token operator\">:</span> <span class=\"token string\">'backup'</span><span class=\"token punctuation\">,</span> <span class=\"token literal-property property\">message</span><span class=\"token operator\">:</span> <span class=\"token string\">'Enter one of your recovery codes.'</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n\n  <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span> <span class=\"token literal-property property\">fallback</span><span class=\"token operator\">:</span> <span class=\"token string\">'recovery'</span><span class=\"token punctuation\">,</span> <span class=\"token literal-property property\">message</span><span class=\"token operator\">:</span> <span class=\"token string\">'Contact support to verify your identity.'</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>Each step down the chain accepts a weaker proof of identity, so the tradeoffs matter. TOTP codes are still strong second factors. Email and SMS one-time codes are vulnerable to interception but are better than locking a user out entirely. Recovery codes are single-use secrets that should be generated at enrollment time, stored offline by the user, and hashed on the server. Administrative recovery should require out-of-band identity verification and should be logged and auditable.</p>\n<p>The critical mistake is treating fallbacks as optional. Users will not register backup methods unless you build it into the enrollment flow. After a user registers their primary WebAuthn credential, prompt them immediately to set up a secondary method. Make it a required step, not a dismissible banner they will ignore.</p>\n<h3 id=\"matching-security-to-risk\" style=\"position:relative;\"><a href=\"#matching-security-to-risk\" aria-label=\"matching security to risk permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Matching Security to Risk</h3>\n<p>Not every action in your application carries the same risk. A user browsing a dashboard does not need the same authentication assurance as one changing account credentials or approving a financial transaction. Progressive enhancement applies here too.</p>\n<p>For routine access, a valid session token is sufficient. For sensitive operations, trigger step-up authentication using the strongest method the user has registered. If they have a WebAuthn credential, require it. If they only have TOTP, require that. This layered approach lets you enforce strong authentication where it matters without adding friction to every interaction.</p>\n<p>The underlying principle is straightforward: make the secure path the default path, design the fallbacks honestly, and never let the absence of one method leave a user without any path forward.</p>\n<h2 id=\"using-webauthn-in-a-plugin-or-auth-framework-eg-supertokens\" style=\"position:relative;\"><a href=\"#using-webauthn-in-a-plugin-or-auth-framework-eg-supertokens\" aria-label=\"using webauthn in a plugin or auth framework eg supertokens permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Using WebAuthn in a Plugin or Auth Framework (e.g. SuperTokens)</h2>\n<p>Implementing WebAuthn from scratch means writing and maintaining challenge generation, CBOR parsing, attestation verification, signature validation, counter tracking, and credential storage. An auth framework handles that machinery for you and exposes WebAuthn as a configurable component. <a href=\"https://supertokens.com/docs/authentication/passkeys/initial-setup\" target=\"_blank\" rel=\"nofollow\">SuperTokens</a> is a good example: it ships WebAuthn (passkeys) as a recipe in its Node.js and Python SDKs, so you enable it the same way you enable email/password or social login.</p>\n<h3 id=\"enabling-webauthn-via-plugin-configuration\" style=\"position:relative;\"><a href=\"#enabling-webauthn-via-plugin-configuration\" aria-label=\"enabling webauthn via plugin configuration permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Enabling WebAuthn via Plugin Configuration</h3>\n<p>In SuperTokens, authentication methods are “recipes” that you add to a <code class=\"language-text\">recipeList</code>. Adding passkey support means adding the WebAuthn recipe to both your backend and frontend initialization.</p>\n<p>Backend (Node.js):</p>\n<div\n              class=\"gatsby-code-button-container\"\n              data-toaster-id=\"36754821982269670000\"\n              data-toaster-class=\"gatsby-code-button-toaster\"\n              data-toaster-text-class=\"gatsby-code-button-toaster-text\"\n              data-toaster-text=\"Copied!\"\n              data-toaster-duration=\"3500\"\n              onClick=\"copyToClipboard(`import supertokens from &quot;supertokens-node&quot;;\nimport Session from &quot;supertokens-node/recipe/session&quot;;\nimport WebAuthn from &quot;supertokens-node/recipe/webauthn&quot;;\n\nsupertokens.init({\n  supertokens: { connectionURI: &quot;...&quot; },\n  appInfo: {\n    appName: &quot;Your App&quot;,\n    apiDomain: &quot;https://api.example.com&quot;,\n    websiteDomain: &quot;https://example.com&quot;\n  },\n  recipeList: [\n    WebAuthn.init(),\n    Session.init()\n  ]\n});`, `36754821982269670000`)\"\n            >\n              <div\n                class=\"gatsby-code-button\"\n                data-tooltip=\"\"\n              >\n                <svg class=\"gatsby-code-button-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0V0z\"/><path d=\"M16 1H2v16h2V3h12V1zm-1 4l6 6v12H6V5h9zm-1 7h5.5L14 6.5V12z\"/></svg>\n              </div>\n            </div>\n<div class=\"gatsby-highlight\" data-language=\"javascript\"><pre class=\"language-javascript\"><code class=\"language-javascript\"><span class=\"token keyword\">import</span> supertokens <span class=\"token keyword\">from</span> <span class=\"token string\">\"supertokens-node\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> Session <span class=\"token keyword\">from</span> <span class=\"token string\">\"supertokens-node/recipe/session\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> WebAuthn <span class=\"token keyword\">from</span> <span class=\"token string\">\"supertokens-node/recipe/webauthn\"</span><span class=\"token punctuation\">;</span>\n\nsupertokens<span class=\"token punctuation\">.</span><span class=\"token function\">init</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span>\n  <span class=\"token literal-property property\">supertokens</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span> <span class=\"token literal-property property\">connectionURI</span><span class=\"token operator\">:</span> <span class=\"token string\">\"...\"</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n  <span class=\"token literal-property property\">appInfo</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token literal-property property\">appName</span><span class=\"token operator\">:</span> <span class=\"token string\">\"Your App\"</span><span class=\"token punctuation\">,</span>\n    <span class=\"token literal-property property\">apiDomain</span><span class=\"token operator\">:</span> <span class=\"token string\">\"https://api.example.com\"</span><span class=\"token punctuation\">,</span>\n    <span class=\"token literal-property property\">websiteDomain</span><span class=\"token operator\">:</span> <span class=\"token string\">\"https://example.com\"</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n  <span class=\"token literal-property property\">recipeList</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">[</span>\n    WebAuthn<span class=\"token punctuation\">.</span><span class=\"token function\">init</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n    Session<span class=\"token punctuation\">.</span><span class=\"token function\">init</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span>\n  <span class=\"token punctuation\">]</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>Frontend (React):</p>\n<div\n              class=\"gatsby-code-button-container\"\n              data-toaster-id=\"64790101257331780000\"\n              data-toaster-class=\"gatsby-code-button-toaster\"\n              data-toaster-text-class=\"gatsby-code-button-toaster-text\"\n              data-toaster-text=\"Copied!\"\n              data-toaster-duration=\"3500\"\n              onClick=\"copyToClipboard(`import SuperTokens from &quot;supertokens-auth-react&quot;;\nimport WebAuthn from &quot;supertokens-auth-react/recipe/webauthn&quot;;\nimport Session from &quot;supertokens-auth-react/recipe/session&quot;;\n\nSuperTokens.init({\n  appInfo: {\n    appName: &quot;Your App&quot;,\n    apiDomain: &quot;https://api.example.com&quot;,\n    websiteDomain: &quot;https://example.com&quot;\n  },\n  recipeList: [\n    WebAuthn.init(),\n    Session.init()\n  ]\n});`, `64790101257331780000`)\"\n            >\n              <div\n                class=\"gatsby-code-button\"\n                data-tooltip=\"\"\n              >\n                <svg class=\"gatsby-code-button-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0V0z\"/><path d=\"M16 1H2v16h2V3h12V1zm-1 4l6 6v12H6V5h9zm-1 7h5.5L14 6.5V12z\"/></svg>\n              </div>\n            </div>\n<div class=\"gatsby-highlight\" data-language=\"javascript\"><pre class=\"language-javascript\"><code class=\"language-javascript\"><span class=\"token keyword\">import</span> SuperTokens <span class=\"token keyword\">from</span> <span class=\"token string\">\"supertokens-auth-react\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> WebAuthn <span class=\"token keyword\">from</span> <span class=\"token string\">\"supertokens-auth-react/recipe/webauthn\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> Session <span class=\"token keyword\">from</span> <span class=\"token string\">\"supertokens-auth-react/recipe/session\"</span><span class=\"token punctuation\">;</span>\n\nSuperTokens<span class=\"token punctuation\">.</span><span class=\"token function\">init</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span>\n  <span class=\"token literal-property property\">appInfo</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token literal-property property\">appName</span><span class=\"token operator\">:</span> <span class=\"token string\">\"Your App\"</span><span class=\"token punctuation\">,</span>\n    <span class=\"token literal-property property\">apiDomain</span><span class=\"token operator\">:</span> <span class=\"token string\">\"https://api.example.com\"</span><span class=\"token punctuation\">,</span>\n    <span class=\"token literal-property property\">websiteDomain</span><span class=\"token operator\">:</span> <span class=\"token string\">\"https://example.com\"</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n  <span class=\"token literal-property property\">recipeList</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">[</span>\n    WebAuthn<span class=\"token punctuation\">.</span><span class=\"token function\">init</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n    Session<span class=\"token punctuation\">.</span><span class=\"token function\">init</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span>\n  <span class=\"token punctuation\">]</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>With those two changes, the framework wires up the API routes for credential options, signup, and signin. The core exposes endpoints like <code class=\"language-text\">POST /recipe/webauthn/options/signin</code> and <code class=\"language-text\">POST /recipe/webauthn/signup</code>, and the SDK handles the browser-side <code class=\"language-text\">navigator.credentials</code> calls and base64url encoding you would otherwise write yourself. If you use the prebuilt UI, the passkey registration and login screens render automatically. If you build custom UI, the SDK exposes functions that drive the same flows.</p>\n<h3 id=\"registration-authentication-errors-and-fallback\" style=\"position:relative;\"><a href=\"#registration-authentication-errors-and-fallback\" aria-label=\"registration authentication errors and fallback permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Registration, Authentication, Errors, and Fallback</h3>\n<p>The recipe maps cleanly to the two WebAuthn ceremonies. On signup, the framework generates options, the SDK calls the authenticator, and the core verifies the attestation and stores the credential against the user. On signin, it generates a challenge, collects the assertion, verifies the signature, and updates the counter.</p>\n<p>Error handling is where framework integration earns its keep. WebAuthn surfaces a defined set of <code class=\"language-text\">DOMException</code> types: <code class=\"language-text\">NotAllowedError</code> when the user cancels or the prompt times out, <code class=\"language-text\">InvalidStateError</code> when a credential is already registered on the device, and <code class=\"language-text\">NotSupportedError</code> when the requested parameters are unavailable. The SDK normalizes these into status responses you can branch on, rather than leaving you to parse raw browser exceptions.</p>\n<p>For fallback, you compose recipes rather than building a parallel system. Pair the WebAuthn recipe with email/password or passwordless (magic link / OTP) recipes in the same <code class=\"language-text\">recipeList</code>. Users on devices without passkey support authenticate through the alternate method, and the framework manages both paths under one user identity. You can also configure the relying party ID, attestation policy, and user verification requirements through the recipe’s customization options to match your security posture.</p>\n<h3 id=\"migration-strategies-for-existing-users\" style=\"position:relative;\"><a href=\"#migration-strategies-for-existing-users\" aria-label=\"migration strategies for existing users permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Migration Strategies for Existing Users</h3>\n<p>You rarely flip an existing user base to WebAuthn overnight. The realistic path is optional, incremental adoption layered on top of your current authentication.</p>\n<p>Start by keeping your existing login intact and offering passkey registration as an opt-in inside account settings. After a user signs in with their password, prompt them to add a passkey for faster future logins. This builds a population of WebAuthn-capable accounts without forcing anyone through a migration wall.</p>\n<p>Once a user has registered a passkey, you can offer it as the default login method on their next visit while keeping the password available as fallback. Track adoption, and for high-value accounts (admins, users with elevated permissions), you can move from optional to required once enough of that cohort has enrolled.</p>\n<p>In SuperTokens specifically, this layering is handled by combining the WebAuthn recipe with your existing first-factor recipe, and optionally with the <a href=\"https://supertokens.com/docs/additional-verification/mfa/webauthn-setup\" target=\"_blank\" rel=\"nofollow\">MFA recipe</a> when you want WebAuthn enforced as a second factor for specific users or roles. The migration becomes a configuration and UX exercise rather than a rewrite of your auth layer.</p>\n<h2 id=\"common-pitfalls-security-considerations-and-best-practices\" style=\"position:relative;\"><a href=\"#common-pitfalls-security-considerations-and-best-practices\" aria-label=\"common pitfalls security considerations and best practices permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Common Pitfalls, Security Considerations, and Best Practices</h2>\n<p>WebAuthn removes entire categories of attack, but it introduces operational details that are easy to get wrong. The failures here are rarely cryptographic. They show up in attestation handling, credential lifecycle management, and error states that leave users stranded.</p>\n<h3 id=\"attestation-consent-and-extensions\" style=\"position:relative;\"><a href=\"#attestation-consent-and-extensions\" aria-label=\"attestation consent and extensions permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Attestation, Consent, and Extensions</h3>\n<p>Attestation is the most commonly over-engineered part of a WebAuthn integration. It lets you verify the make and model of an authenticator, but most applications do not need it. Requesting <code class=\"language-text\">attestation: \"direct\"</code> pulls back identifying information about the user’s device, which raises privacy concerns and adds verification complexity you have to maintain. Unless you operate in a regulated environment that mandates specific hardware (FIPS-validated keys, for example), set <code class=\"language-text\">attestation: \"none\"</code>. This respects user privacy and simplifies your server logic without weakening the core security guarantee.</p>\n<p>User consent is built into the protocol through user presence and user verification. User presence (a touch or tap) confirms a human is physically present. User verification (biometric or PIN) confirms it is the right human. Set <code class=\"language-text\">userVerification: \"preferred\"</code> for most flows so the authenticator uses verification when available without hard-failing on devices that lack it. Reserve <code class=\"language-text\">\"required\"</code> for sensitive operations where you cannot accept presence alone.</p>\n<p>Extensions like the PRF extension (for deriving encryption keys) or <code class=\"language-text\">credProps</code> (for learning whether a credential is discoverable) are powerful but unevenly supported. Treat them as enhancements, not dependencies, and always check the extension results rather than assuming they were honored.</p>\n<h3 id=\"key-rotation-and-credential-revocation\" style=\"position:relative;\"><a href=\"#key-rotation-and-credential-revocation\" aria-label=\"key rotation and credential revocation permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Key Rotation and Credential Revocation</h3>\n<p>WebAuthn credentials do not expire on their own, so revocation is your responsibility. Build a credential management interface where users can view their registered authenticators and remove ones they no longer control. When a user reports a lost device, revoking the corresponding credential should immediately invalidate it server-side by deleting the stored public key.</p>\n<p>There is no “rotating” a WebAuthn private key the way you rotate a password, because the key never leaves the authenticator. Rotation in practice means registering a new credential and revoking the old one. Encourage users to register a replacement before removing an old authenticator so they never drop to zero registered credentials.</p>\n<h3 id=\"multi-device-registration\" style=\"position:relative;\"><a href=\"#multi-device-registration\" aria-label=\"multi device registration permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Multi-Device Registration</h3>\n<p>A single registered authenticator is a single point of failure. If it is the only credential and the device is lost, the user is locked out. Make multi-device registration a deliberate part of onboarding: after a user registers their first credential, prompt them to add a second (a phone passkey alongside a laptop sensor, or a backup security key). Store credentials in a one-to-many relationship with the user, and let users assign friendly names so they can tell their devices apart when managing them later.</p>\n<p>Synced passkeys (backed up through iCloud Keychain or Google Password Manager) reduce this risk because the credential follows the user across their devices. Device-bound credentials like hardware security keys do not sync, so backup registration matters more for those.</p>\n<h3 id=\"error-handling\" style=\"position:relative;\"><a href=\"#error-handling\" aria-label=\"error handling permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Error Handling</h3>\n<p>WebAuthn surfaces failures as <code class=\"language-text\">DOMException</code> types, and handling them gracefully separates a usable flow from a frustrating one.</p>\n<div\n              class=\"gatsby-code-button-container\"\n              data-toaster-id=\"59216623582186360000\"\n              data-toaster-class=\"gatsby-code-button-toaster\"\n              data-toaster-text-class=\"gatsby-code-button-toaster-text\"\n              data-toaster-text=\"Copied!\"\n              data-toaster-duration=\"3500\"\n              onClick=\"copyToClipboard(`try {\n  const credential = await navigator.credentials.get({ publicKey: options });\n  // verify on server\n} catch (err) {\n  switch (err.name) {\n    case 'NotAllowedError':\n      // User canceled or the operation timed out\n      showMessage('Authentication canceled. Try again or use another method.');\n      break;\n    case 'InvalidStateError':\n      // Credential already registered on this device (during create)\n      showMessage('This device is already registered.');\n      break;\n    case 'NotSupportedError':\n      // Requested parameters not supported\n      offerFallbackMethod();\n      break;\n    case 'SecurityError':\n      // Origin or RP ID mismatch\n      logSecurityEvent(err);\n      break;\n    default:\n      offerFallbackMethod();\n  }\n}`, `59216623582186360000`)\"\n            >\n              <div\n                class=\"gatsby-code-button\"\n                data-tooltip=\"\"\n              >\n                <svg class=\"gatsby-code-button-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0V0z\"/><path d=\"M16 1H2v16h2V3h12V1zm-1 4l6 6v12H6V5h9zm-1 7h5.5L14 6.5V12z\"/></svg>\n              </div>\n            </div>\n<div class=\"gatsby-highlight\" data-language=\"javascript\"><pre class=\"language-javascript\"><code class=\"language-javascript\"><span class=\"token keyword\">try</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> credential <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> navigator<span class=\"token punctuation\">.</span>credentials<span class=\"token punctuation\">.</span><span class=\"token function\">get</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span> <span class=\"token literal-property property\">publicKey</span><span class=\"token operator\">:</span> options <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token comment\">// verify on server</span>\n<span class=\"token punctuation\">}</span> <span class=\"token keyword\">catch</span> <span class=\"token punctuation\">(</span>err<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">switch</span> <span class=\"token punctuation\">(</span>err<span class=\"token punctuation\">.</span>name<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">case</span> <span class=\"token string\">'NotAllowedError'</span><span class=\"token operator\">:</span>\n      <span class=\"token comment\">// User canceled or the operation timed out</span>\n      <span class=\"token function\">showMessage</span><span class=\"token punctuation\">(</span><span class=\"token string\">'Authentication canceled. Try again or use another method.'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n      <span class=\"token keyword\">break</span><span class=\"token punctuation\">;</span>\n    <span class=\"token keyword\">case</span> <span class=\"token string\">'InvalidStateError'</span><span class=\"token operator\">:</span>\n      <span class=\"token comment\">// Credential already registered on this device (during create)</span>\n      <span class=\"token function\">showMessage</span><span class=\"token punctuation\">(</span><span class=\"token string\">'This device is already registered.'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n      <span class=\"token keyword\">break</span><span class=\"token punctuation\">;</span>\n    <span class=\"token keyword\">case</span> <span class=\"token string\">'NotSupportedError'</span><span class=\"token operator\">:</span>\n      <span class=\"token comment\">// Requested parameters not supported</span>\n      <span class=\"token function\">offerFallbackMethod</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n      <span class=\"token keyword\">break</span><span class=\"token punctuation\">;</span>\n    <span class=\"token keyword\">case</span> <span class=\"token string\">'SecurityError'</span><span class=\"token operator\">:</span>\n      <span class=\"token comment\">// Origin or RP ID mismatch</span>\n      <span class=\"token function\">logSecurityEvent</span><span class=\"token punctuation\">(</span>err<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n      <span class=\"token keyword\">break</span><span class=\"token punctuation\">;</span>\n    <span class=\"token keyword\">default</span><span class=\"token operator\">:</span>\n      <span class=\"token function\">offerFallbackMethod</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p><code class=\"language-text\">NotAllowedError</code> is the one you will see most. It covers both user cancellation and timeouts, so do not treat it as an attack signal. Give the user a clear path to retry or fall back. Authenticator removal mid-flow (unplugging a security key) also surfaces here. Always pair a timeout value with a fallback option so a stalled prompt never becomes a dead end.</p>\n<h3 id=\"phishing-resistance-is-not-automatic-configuration-safety\" style=\"position:relative;\"><a href=\"#phishing-resistance-is-not-automatic-configuration-safety\" aria-label=\"phishing resistance is not automatic configuration safety permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Phishing Resistance Is Not Automatic Configuration Safety</h3>\n<p>Origin binding is what makes WebAuthn phishing-resistant: a credential registered for <code class=\"language-text\">example.com</code> cannot be used on <code class=\"language-text\">evil-example.com</code>, because the browser includes the real origin in the signed data and the authenticator will not match credentials across origins. A credential captured by a phishing site is useless because it was never issued for that origin in the first place.</p>\n<p>That protection depends on configuring your relying party ID and expected origin correctly. The most common self-inflicted wound is loose origin validation on the server. Never derive your expected origin from the incoming request’s <code class=\"language-text\">Origin</code> header, because an attacker controls that value. Hardcode your allowed origins or load them from server configuration, and validate strictly.</p>\n<div\n              class=\"gatsby-code-button-container\"\n              data-toaster-id=\"83718322407612190000\"\n              data-toaster-class=\"gatsby-code-button-toaster\"\n              data-toaster-text-class=\"gatsby-code-button-toaster-text\"\n              data-toaster-text=\"Copied!\"\n              data-toaster-duration=\"3500\"\n              onClick=\"copyToClipboard(`// WRONG: trusting attacker-controlled input\nexpectedOrigin: req.headers.origin\n\n// CORRECT: server-defined allowlist\nconst ALLOWED_ORIGINS = process.env.NODE_ENV === 'production'\n  ? ['https://example.com']\n  : ['http://localhost:3000'];`, `83718322407612190000`)\"\n            >\n              <div\n                class=\"gatsby-code-button\"\n                data-tooltip=\"\"\n              >\n                <svg class=\"gatsby-code-button-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path fill=\"none\" d=\"M0 0h24v24H0V0z\"/><path d=\"M16 1H2v16h2V3h12V1zm-1 4l6 6v12H6V5h9zm-1 7h5.5L14 6.5V12z\"/></svg>\n              </div>\n            </div>\n<div class=\"gatsby-highlight\" data-language=\"javascript\"><pre class=\"language-javascript\"><code class=\"language-javascript\"><span class=\"token comment\">// WRONG: trusting attacker-controlled input</span>\n<span class=\"token literal-property property\">expectedOrigin</span><span class=\"token operator\">:</span> req<span class=\"token punctuation\">.</span>headers<span class=\"token punctuation\">.</span>origin\n\n<span class=\"token comment\">// CORRECT: server-defined allowlist</span>\n<span class=\"token keyword\">const</span> <span class=\"token constant\">ALLOWED_ORIGINS</span> <span class=\"token operator\">=</span> process<span class=\"token punctuation\">.</span>env<span class=\"token punctuation\">.</span><span class=\"token constant\">NODE_ENV</span> <span class=\"token operator\">===</span> <span class=\"token string\">'production'</span>\n  <span class=\"token operator\">?</span> <span class=\"token punctuation\">[</span><span class=\"token string\">'https://example.com'</span><span class=\"token punctuation\">]</span>\n  <span class=\"token operator\">:</span> <span class=\"token punctuation\">[</span><span class=\"token string\">'http://localhost:3000'</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>A mismatched or overly broad relying party ID (allowing wildcard subdomains, for instance) can also create silent failures or widen your attack surface. Set the RP ID to the registrable domain you actually serve, test it against your real origin, and resist the urge to loosen it to make a misconfiguration “work.”</p>\n<h2 id=\"webauthn-vs-other-auth-methods-when-should-you-use-it\" style=\"position:relative;\"><a href=\"#webauthn-vs-other-auth-methods-when-should-you-use-it\" aria-label=\"webauthn vs other auth methods when should you use it permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>WebAuthn vs Other Auth Methods: When Should You Use It?</h2>\n<p>WebAuthn is the strongest browser-based authentication available, but “strongest” does not mean “right for every situation.” Choosing it well means understanding what it beats, where it shines, and what it costs.</p>\n<h3 id=\"how-it-compares\" style=\"position:relative;\"><a href=\"#how-it-compares\" aria-label=\"how it compares permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>How It Compares</h3>\n<p>The decisive difference between WebAuthn and every password-derived method is the shared secret. Passwords, OTP codes, and TOTP seeds are all secrets that exist in two places and can be intercepted, phished, or leaked. WebAuthn’s private key never leaves the authenticator, so there is nothing to intercept.</p>\n<table>\n<thead>\n<tr>\n<th>Method</th>\n<th>Phishing-resistant</th>\n<th>Shared secret</th>\n<th>Breach exposure</th>\n<th>User friction</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Password only</td>\n<td>No</td>\n<td>Yes</td>\n<td>High (stored hashes)</td>\n<td>Low</td>\n</tr>\n<tr>\n<td>Password + SMS OTP</td>\n<td>No</td>\n<td>Yes</td>\n<td>Medium (interceptable)</td>\n<td>Medium</td>\n</tr>\n<tr>\n<td>Password + TOTP</td>\n<td>Partial</td>\n<td>Yes (seed)</td>\n<td>Medium (seed theft)</td>\n<td>Medium</td>\n</tr>\n<tr>\n<td>WebAuthn</td>\n<td>Yes</td>\n<td>No</td>\n<td>Minimal (public keys only)</td>\n<td>Low to medium</td>\n</tr>\n</tbody>\n</table>\n<p>Against password-plus-SMS-OTP, WebAuthn wins decisively. SMS codes are vulnerable to SIM swapping and interception, and the password underneath remains phishable. TOTP (authenticator-app codes) is stronger because the code is generated locally and never transmitted as a long-lived secret, but the shared seed can still be stolen at enrollment, and a user can be tricked into typing a valid code into a phishing page. WebAuthn closes that gap entirely: even a user who wants to authenticate on a phishing site cannot, because the browser will not release a credential bound to a different origin.</p>\n<p>Compared to traditional MFA generally, the difference is architectural rather than incremental. Traditional MFA stacks a second shared secret on top of the first. WebAuthn replaces the secret model with possession of a private key plus local user verification. That is why it resists the entire phishing and credential-stuffing class of attacks rather than just raising the cost.</p>\n<h3 id=\"where-webauthn-is-the-right-choice\" style=\"position:relative;\"><a href=\"#where-webauthn-is-the-right-choice\" aria-label=\"where webauthn is the right choice permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Where WebAuthn Is the Right Choice</h3>\n<p>WebAuthn is the strongest fit anywhere phishing resistance is a hard requirement. High-value targets benefit most: admin consoles, financial and healthcare applications, developer infrastructure, and any system where a compromised account causes serious downstream damage. It is also a strong choice for step-up authentication on sensitive operations, where you want cryptographic assurance for actions like changing credentials or approving transactions.</p>\n<p>Consumer-facing products with broad device support are increasingly good candidates too, especially using synced passkeys. The login experience (a fingerprint or face scan) is often faster and smoother than typing a password plus an OTP code, so the security upgrade can actually reduce friction rather than add it.</p>\n<h3 id=\"the-tradeoffs-are-real\" style=\"position:relative;\"><a href=\"#the-tradeoffs-are-real\" aria-label=\"the tradeoffs are real permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The Tradeoffs Are Real</h3>\n<p>Device availability is the main constraint. Not every user has a biometric sensor or a security key, and users logging in from shared or borrowed machines may not have access to their authenticator. This is why a fallback path is mandatory rather than optional, as covered earlier.</p>\n<p>Adoption friction is the second cost. Users are unfamiliar with passkeys, and account recovery becomes more involved when there is no password to reset. A lost device with no backup credential and no recovery method means a locked-out user. You absorb this cost through deliberate onboarding: prompt for a backup credential at registration, provide recovery codes, and keep a well-designed fallback chain in place.</p>\n<p>The practical answer for most teams is not “WebAuthn or nothing.” It is WebAuthn as the preferred, default method, backed by TOTP and recovery options for the cases where it is unavailable. You get phishing-resistant authentication for the users and moments that matter most, without locking out anyone who cannot use it yet.</p>\n<h2 id=\"faq--people-also-ask\" style=\"position:relative;\"><a href=\"#faq--people-also-ask\" aria-label=\"faq  people also ask permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>FAQ / People Also Ask</h2>\n<h3 id=\"what-is-the-difference-between-webauthn-and-fido2\" style=\"position:relative;\"><a href=\"#what-is-the-difference-between-webauthn-and-fido2\" aria-label=\"what is the difference between webauthn and fido2 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>What is the difference between WebAuthn and FIDO2?</h3>\n<p>FIDO2 is the overall framework; WebAuthn is one of its two components. FIDO2 combines WebAuthn (the W3C API that browsers and web applications use) with CTAP, the Client-to-Authenticator Protocol that handles communication between the device and external authenticators like security keys. In short, WebAuthn is what your web application calls, and CTAP is how the browser talks to the hardware. When people say “FIDO2 authentication,” they are usually describing the full passwordless experience that WebAuthn enables.</p>\n<h3 id=\"does-webauthn-work-on-mobile-ios-and-android-browsers\" style=\"position:relative;\"><a href=\"#does-webauthn-work-on-mobile-ios-and-android-browsers\" aria-label=\"does webauthn work on mobile ios and android browsers permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Does WebAuthn work on mobile, iOS, and Android browsers?</h3>\n<p>Yes. iOS supports WebAuthn through Safari using Face ID and Touch ID, and Android supports it through Chrome and the platform credential manager using fingerprint and PIN. Both platforms back synced passkeys (iCloud Keychain on Apple devices, Google Password Manager on Android), so a credential registered on a phone can follow the user across their devices. Mobile is one of the strongest environments for WebAuthn precisely because nearly every modern phone ships with a built-in biometric authenticator.</p>\n<h3 id=\"can-webauthn-be-used-as-a-second-factor\" style=\"position:relative;\"><a href=\"#can-webauthn-be-used-as-a-second-factor\" aria-label=\"can webauthn be used as a second factor permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Can WebAuthn be used as a second factor?</h3>\n<p>Yes. WebAuthn supports both single-factor (passwordless) and second-factor use. As a second factor, it replaces weaker options like SMS or TOTP with a phishing-resistant cryptographic check after a primary login. Many teams adopt it this way first, layering a WebAuthn security key or passkey on top of an existing password or social login, then move toward passwordless once adoption grows. Frameworks like SuperTokens let you configure WebAuthn as a required second factor for specific users or roles through their MFA recipe.</p>\n<h3 id=\"what-happens-if-i-lose-my-device-with-webauthn-credentials\" style=\"position:relative;\"><a href=\"#what-happens-if-i-lose-my-device-with-webauthn-credentials\" aria-label=\"what happens if i lose my device with webauthn credentials permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>What happens if I lose my device with WebAuthn credentials?</h3>\n<p>It depends on the type of credential. Synced passkeys are backed up to your platform account (iCloud or Google), so they are recoverable on a new device by signing back into that account. Device-bound credentials, like hardware security keys, are not recoverable; the private key is gone with the device. This is why backup matters. A well-designed implementation prompts you to register a second credential at enrollment and provides recovery codes, so losing one authenticator never means losing access to your account. If you do lose your only credential with no backup, you fall back to the application’s account recovery process, which should require out-of-band identity verification.</p>\n<h2 id=\"getting-started\" style=\"position:relative;\"><a href=\"#getting-started\" aria-label=\"getting started permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Getting Started</h2>\n<p>WebAuthn looks complex from the spec, but adding it to a real application is mostly a matter of choosing the right tools and wiring them in. You do not need to implement CBOR parsing or signature verification yourself. Here is the fastest path from reading to shipping.</p>\n<h3 id=\"pick-a-library\" style=\"position:relative;\"><a href=\"#pick-a-library\" aria-label=\"pick a library permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Pick a Library</h3>\n<p>If you are building the flow directly, <a href=\"https://github.com/MasterKale/SimpleWebAuthn\" target=\"_blank\" rel=\"nofollow\"><code class=\"language-text\">@simplewebauthn</code></a> is the most widely used option in the JavaScript ecosystem. It provides <code class=\"language-text\">@simplewebauthn/server</code> for backend verification and <code class=\"language-text\">@simplewebauthn/browser</code> for the frontend, handling the encoding and cryptographic work described earlier in this guide. For other stacks, the <a href=\"https://fidoalliance.org/certification/fido-certified-products/\" target=\"_blank\" rel=\"nofollow\">FIDO Alliance maintains a list</a> of certified server libraries.</p>\n<p>If you would rather not manage the auth layer at all, use a framework that ships WebAuthn as a configurable component. <a href=\"https://supertokens.com/docs/authentication/passkeys/initial-setup\" target=\"_blank\" rel=\"nofollow\">SuperTokens</a> provides passkey support as a recipe in its Node.js and Python SDKs. Enabling it is a matter of adding <code class=\"language-text\">WebAuthn.init()</code> to your backend and frontend <code class=\"language-text\">recipeList</code>, as shown earlier. Because it is open source, you keep full visibility into the backend logic with no vendor lock-in, and you can self-host the core if you need to.</p>\n<h3 id=\"add-webauthn-to-your-existing-auth\" style=\"position:relative;\"><a href=\"#add-webauthn-to-your-existing-auth\" aria-label=\"add webauthn to your existing auth permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Add WebAuthn to Your Existing Auth</h3>\n<p>You do not have to rebuild your login to adopt WebAuthn. The lowest-risk starting point is to layer it on top of what you already have:</p>\n<ol>\n<li>Keep your current login (password, social, or passwordless) in place.</li>\n<li>Add passkey registration as an opt-in inside account settings.</li>\n<li>After a user registers a passkey, offer it as the default login on their next visit, with the existing method as fallback.</li>\n<li>For high-value accounts, move from optional to required once adoption is healthy.</li>\n</ol>\n<p>In SuperTokens, this means composing the WebAuthn recipe alongside your existing first-factor recipe, and optionally the <a href=\"https://supertokens.com/docs/additional-verification/mfa/webauthn-setup\" target=\"_blank\" rel=\"nofollow\">MFA recipe</a> when you want it enforced as a second factor. The migration becomes a configuration and UX exercise rather than a rewrite.</p>\n<h3 id=\"resources\" style=\"position:relative;\"><a href=\"#resources\" aria-label=\"resources permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Resources</h3>\n<p>For deeper reference as you build:</p>\n<ul>\n<li><a href=\"https://www.w3.org/TR/webauthn-3/\" target=\"_blank\" rel=\"nofollow\">W3C WebAuthn Level 3 specification</a>: the authoritative source on the API and its data structures.</li>\n<li><a href=\"https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API\" target=\"_blank\" rel=\"nofollow\">MDN Web Authentication API docs</a>: practical, example-driven reference for the browser API.</li>\n<li><a href=\"https://supertokens.com/docs/authentication/passkeys/initial-setup\" target=\"_blank\" rel=\"nofollow\">SuperTokens passkeys documentation</a>: setup, customization, and MFA configuration.</li>\n<li><a href=\"https://fidoalliance.org/fido2-2/fido2-web-authentication-webauthn/\" target=\"_blank\" rel=\"nofollow\">FIDO Alliance</a>: background on FIDO2, CTAP, and the broader passwordless ecosystem.</li>\n<li><a href=\"https://passkeys.dev/\" target=\"_blank\" rel=\"nofollow\">passkeys.dev</a>: community guides and device support tables maintained alongside the FIDO Alliance and W3C.</li>\n</ul>\n<p>The shift away from shared secrets is already underway. Apple, Google, and Microsoft all support passkeys across their platforms, and the libraries and frameworks to implement WebAuthn are mature and well documented. Start by adding passkey registration as an option for your existing users, measure adoption, and expand from there. Phishing-resistant authentication is no longer a project reserved for large security teams; it is a recipe you can enable this week.</p>\n<!-- Section word count: 461 -->\n<!-- Total word count: 5,829 -->","frontmatter":{"date":"June 09, 2026","title":"What Is the WebAuthn API (and How to Use It in Your Auth Flow)","cover":"webauthn_api.png","author":"Maurice Saldivar","description":"Learn how the WebAuthn API enables passwordless, phishing-resistant login. Get step-by-step code and how to plug it into your auth stack."},"fields":{"slug":"/webauthin-api/"}},"site":{"siteMetadata":{"title":"SuperTokens Blog"}}},"pageContext":{"id":"0cd53134-8342-59a9-ac4c-111e9a524d36","fields__slug":"/webauthin-api/","__params":{"fields__slug":"webauthin-api"}}},
    "staticQueryHashes": []}