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:
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 */ });