/
Contact usPricingLog in
Node
​

    About Stytch

    Introduction
    Integration Approaches
      Full-stack overview
      Frontend (pre-built UI)
      Frontend (headless)
      Backend
    Migrations
      Migration overview
      Migrating users statically
      Migrating users dynamically
      Additional migration considerations
      Zero-downtime deployment
      Defining external IDs for users
      Migrating from Stytch Consumer to B2B
      Exporting from Stytch
    Branding
      Custom domains
      Custom email templates

    Authentication

    DFP Protected Auth
      Overview
      Setting up DFP Protected Auth
      Handling challenges
    Magic Links
    • Email Magic Links

      • Getting started with the API
        Getting started with the SDK
        Replacing your password reset flow
        Building an invite user flow
        Add magic links to an existing auth flow
        Adding PKCE to a Magic Link flow
        Magic Link redirect routing
    • Embeddable Magic Links

      • Getting started with the API
    MFA
      Overview
      Backend integration
      Frontend integration
      Remembered device flow
    Mobile Biometrics
      Overview
    M2M Authentication
      Authenticate an M2M Client
      Rotate client secrets
      Import M2M Clients from Auth0
      Custom claims
    OAuth
    • Identity providers

      • Overview
        Provider setup
      Getting started with the API (Google)
      Add Google One Tap via the SDK
      Email address behavior
      Adding PKCE to an OAuth flow
    Connected Apps
      Overview
      Getting started with the SDK
      Getting started with the API
      Client types
      OAuth scopes
    • Integration Guides

      • Integrate with an Existing Auth System
        MCP Authorization Overview
        Integrate with MCP servers deployed on Cloudflare
        Integrate with MCP servers on Vercel
        Integrate with CLI Apps
        Integrate with AI agents
    • Resources

      • Consent Management
        Custom Domains
        Testing Integrations
    Passcodes
      Getting started with the API
      Getting started with the SDK
    • Toll fraud

      • What is SMS toll fraud?
        How you can prevent toll fraud
      Unsupported countries
    Passkeys & WebAuthn
    • Passkeys

      • Passkeys overview
        Set up Passkeys with the frontend SDK
    • WebAuthn

      • Getting started with the API
        Getting started with the SDK
    Passwords
      Getting started with the API
      Getting started with the SDK
      Password strength policy
    • Email verification

      • Overview
        Email verification before password creation
        Email verification after password creation
    Sessions
      How to use sessions
      Backend integrations
      Frontend integrations
      Custom claims
      Custom claim templates
      Session tokens vs JWTs
      How to use Stytch JWTs
    TOTP
      Getting started with the API
      Getting started with the SDK
    Web3
      Getting started with the API
      Getting started with the SDK
    Trusted Auth Tokens
      Overview
      Getting Started with External IDPs
      Getting Started with Custom Auth Factors
    Device History
      New device notifications

    RBAC

    Resources
      Overview
      Role assignment
    Integration Guides
      Start here
      Backend integration
      Headless frontend integration
      (Legacy) Implement RBAC with metadata

    3rd Party Integrations

    Planetscale
    Supabase
    Feathery
    Unit

    Testing

    E2E testing
    Sandbox values
Get support on SlackVisit our developer forum

Contact us

Consumer Authentication

/

Guides

/

Authentication

/

Passkeys & WebAuthn

/

WebAuthn

/

Getting started with the API

Setting up WebAuthn

This guide describes how to implement both WebAuthn registration and WebAuthn authentication when using a backend Stytch integration. It also provides simplified example code for both the backend and frontend pieces of a WebAuthn integration.

We also recommend checking out our backend WebAuthn example app to accompany this guide:

  • Hosted demo
  • WebAuthn registration code
  • WebAuthn authentication code

If you get stuck while working through this guide, feel free to ask questions in our forum, via support@stytch.com, or in our Slack community.

Step 1: Create Stytch User

If the user attempting to register isn't yet associated with a Stytch user_id, you'll have to create a new Stytch User via the Create User endpoint. The resulting user_id will be used to register a new WebAuthn authenticator.

const stytch = require("stytch");

const client = new stytch.Client({
    project_id: "PROJECT_ID",
    secret: "SECRET",
  }
);

const params = {
    email: "sandbox@stytch.com",
};
client.users.create(params)
    .then(resp => {
        console.log(resp)
    })
    .catch(err => {
        console.log(err)
    });

Step 2: Register a WebAuthn authenticator

To authenticate with WebAuthn, you first need to register an authenticator.

First, you'll make a request to the Start WebAuthn registration endpoint. You need two fields for the request: a Stytch user_id and your login page's domain. When using built-in browser methods like navigator.credentials.create() and navigator.credentials.get(), as we will in this guide, you'll also need to set the use_base64_url_encoding option to true.

Next, you'll create a public key using the public_key_credential_creation_options from the Start WebAuthn registration response and use that to call the browser's built-in navigator.credentials.create() method.

If the navigator.credentials.create() call is successful, pass the resulting public key credential into our Register WebAuthn endpoint.

Here is simplified backend and frontend example code demonstrating the full WebAuthn registration flow:

// Backend code

export async function callWebauthnRegisterStart() {
 const response = await stytchClient.webauthn.registerStart({
   user_id: "user-test-16d9ba61-97a1-4ba4-9720-b03761dc50c6",
   domain: "example.com",
   use_base64_url_encoding: true,
 });

 return response.json();
}

export async function callWebauthnRegister(public_key_credential) {
 const response = await stytchClient.webauthn.register({
   user_id: "user-test-16d9ba61-97a1-4ba4-9720-b03761dc50c6",
   public_key_credential: public_key_credential,
 });

 return response.json();
}
// Frontend code

const webAuthnRegisterStartResponse = await callWebauthnRegisterStart();
const options = webAuthnRegisterStartResponse.public_key_credential_creation_options;

const publicKey = PublicKeyCredential.parseCreationOptionsFromJSON(JSON.parse(options));
const credential = (await navigator.credentials.create({publicKey})) as PublicKeyCredential;

await callWebauthnRegister(public_key_credential: JSON.stringify(credential.toJSON()))
  .then(resp => { /* WebAuthn authenticator successfully registered */ })
  .catch(err => { /* Registration error */ });

Step 3: Authenticate WebAuthn

Now that user has an active WebAuthn registration, you can use it for authentication.

First, you'll make a request to the Start WebAuthn authentication endpoint. Similarly to in Step 2, when using built-in browser methods, you'll need to set the use_base64_url_encoding option to true.

Next, you'll create a public key using the public_key_credential_request_options from the Start WebAuthn authentication response and use that to call the browser's built-in navigator.credentials.get() method.

If the navigator.credentials.get() call is successful, pass the resulting public key credential into our Authenticate WebAuthn endpoint. If the Authenticate WebAuthn call succeeds, your user has successfully authenticated.

Here is simplified backend and frontend example code demonstrating the full WebAuthn authentication flow:

// Backend code

export async function callWebauthnAuthenticateStart() {
 const response = await stytchClient.webauthn.authenticateStart({
   domain: "example.com",
   use_base64_url_encoding: true,
 });

 return response.json();
}

export async function callWebauthnAuthenticate(public_key_credential) {
 const response = await stytchClient.webauthn.authenticate({
   public_key_credential: public_key_credential,
   session_duration_minutes: 60,
 })

 return response.json();
}
// Frontend code

const webAuthnAuthenticateStartResponse = await callWebauthnAuthenticateStart();
const options = webAuthnAuthenticateStartResponse.public_key_credential_request_options;

const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(JSON.parse(options));
const credential = (await navigator.credentials.get({publicKey})) as PublicKeyCredential;

await callWebauthnAuthenticate(JSON.stringify(credential.toJSON()))
  .then(resp => { /* User has successfully authenticated */ })
  .catch(err => { /* Authentication error */ });

Step 4: [Optional but recommended] Manually serialize the public key credentials

In the above registration and authentication steps, we serialized the public key credentials by simply calling credential.toJSON(). This works in most cases, but there are some known incompatibilities with certain password managers and the public key credential's toJSON() method.

To avoid these incompatibilities, you can manually serialize the public key credentials instead of calling credential.toJSON(). Public key credential serialization code will look something like this, where the SerializedAttestationCredential is used in the WebAuthn registration request and the SerializedAssertionCredential is used in the WebAuthn authentication request:

export interface SerializedAttestationCredential {
  id: string;
  rawId: string;
  type: string;
  response: {
    clientDataJSON: string;
    attestationObject: string;
  };
  authenticatorAttachment?: "platform" | "cross-platform";
}

export function serializeAttestationCredential(
  credential: PublicKeyCredential,
): SerializedAttestationCredential {
  const response = credential.response as AuthenticatorAttestationResponse;

  return {
    id: credential.id,
    rawId: base64urlEncode(credential.rawId),
    type: credential.type,
    response: {
      clientDataJSON: base64urlEncode(response.clientDataJSON),
      attestationObject: base64urlEncode(response.attestationObject),
    },
    authenticatorAttachment:
      (credential as any).authenticatorAttachment ?? undefined,
  };
}

export interface SerializedAssertionCredential {
  id: string;
  rawId: string;
  type: string;
  response: {
    clientDataJSON: string;
    authenticatorData: string;
    signature: string;
    userHandle: string | null;
  };
  authenticatorAttachment?: "platform" | "cross-platform";
}

export function serializeAssertionCredential(
  credential: PublicKeyCredential,
): SerializedAssertionCredential {
  const response = credential.response as AuthenticatorAssertionResponse;

  return {
    id: credential.id,
    rawId: base64urlEncode(credential.rawId),
    type: credential.type,
    response: {
      clientDataJSON: base64urlEncode(response.clientDataJSON),
      authenticatorData: base64urlEncode(response.authenticatorData),
      signature: base64urlEncode(response.signature),
      userHandle: response.userHandle
        ? base64urlEncode(response.userHandle)
        : null,
    },
    authenticatorAttachment:
      (credential as any).authenticatorAttachment ?? undefined,
  };
}

function base64urlEncode(buffer: ArrayBuffer): string {
  const bytes = new Uint8Array(buffer);
  let binary = "";

  for (let i = 0; i < bytes.length; i++) {
    binary += String.fromCharCode(bytes[i]);
  }

  return btoa(binary)
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/g, "");
}

Here's updated frontend registration and authentication code that uses the above manual serialization methods instead of toJSON():

// Registration

const webAuthnRegisterStartResponse = await callWebauthnRegisterStart();
const options = webAuthnRegisterStartResponse.public_key_credential_creation_options;

const publicKey = PublicKeyCredential.parseCreationOptionsFromJSON(JSON.parse(options));
const credential = (await navigator.credentials.create({publicKey})) as PublicKeyCredential;

const serializedCredential = serializeAttestationCredential(
  credential as PublicKeyCredential,
);

await callWebauthnRegister(JSON.stringify(serializedCredential))
  .then(resp => { /* WebAuthn authenticator successfully registered */ })
  .catch(err => { /* Registration error */ });
// Authentication

const webAuthnAuthenticateStartResponse = await callWebauthnAuthenticateStart();
const options = webAuthnAuthenticateStartResponse.public_key_credential_request_options;

const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(JSON.parse(options));
const credential = (await navigator.credentials.get({publicKey})) as PublicKeyCredential;

const serializedCredential = serializeAssertionCredential(
  credential as PublicKeyCredential,
);

await callWebauthnAuthenticate(JSON.stringify(serializedCredential))
  .then(resp => { /* User has successfully authenticated */ })
  .catch(err => { /* Authentication error */ });

Step 1: Create Stytch User

Step 2: Register a WebAuthn authenticator

Step 3: Authenticate WebAuthn

Step 4: [Optional but recommended] Manually serialize the public key credentials