8 Min.

How to add Passkeys to your Vue.js App

Passkey logins have proven to be a more secure and smoother way for users to sign in to applications. If you're looking to add a passkey authentication to your Vue.js app, you're in the right place. In this tutorial, we'll walk you through the process of integrating a passkey login into your Vue.js app using the Hanko Passkey API.

To bring passkeys to your app, you'll typically need two things:

  1. A backend to handle the authentication flow and store your users' passkey. We'll be using Express.js for our backend, but the process will be pretty similar for any other Node.js based framework you might be using.
  2. A couple of functions on your Vue.js frontend to display and handle the "Sign in with a passkey" and "Register a passkey" dialogs.

To make things even simpler, we have created a sample app that you can use as a reference throughout the tutorial.

Passkey registration

Express Backend:

Make sure you have the passkey SDK installed in your backend.

npm i @teamhanko/passkeys-sdk

The startServerPasskeyRegistration function is an asynchronous function that takes a userID parameter. It performs the following steps:

  1. Retrieves the user object from the db.users array based on the provided userID.
  2. Calls the passkeyApi.registration.initialize method with the user's ID and email (or an empty string if email is not available) to initialize the passkey registration process.
  3. Returns the createOptions object received from the SDK, which contains the necessary data to proceed with the registration on the client-side.

The finishServerPasskeyRegistration function is another asynchronous function that takes a credential parameter. It simply calls the passkeyApi.registration.finalize method with the provided credential to complete the passkey registration process on the server-side.

These functions act as service-level functions that encapsulate the logic for interacting with the Hanko Passkey SDK. They can be called from controller functions, which in turn are invoked by the corresponding routes in your application.

// services/passkey.js

import { tenant } from "@teamhanko/passkeys-sdk";
import dotenv from "dotenv";
import db from "../db.js";


const passkeyApi = tenant({
  apiKey: process.env.PASSKEYS_API_KEY,
  tenantId: process.env.PASSKEYS_TENANT_ID,

async function startServerPasskeyRegistration(userID) {
  const user = db.users.find((user) => === userID);

  const createOptions = await passkeyApi.registration.initialize({
    username: || "",

  return createOptions;

async function finishServerPasskeyRegistration(credential) {
  await passkeyApi.registration.finalize(credential);

The handlePasskeyRegister() controller function first extracts the user object from the request (req) and retrieves the userID from it.

Next, it extracts the start, finish, and credential properties from the request body. These properties are used to determine the specific action to be performed.

If the start property is present, the controller function calls thestartServerPasskeyRegistration service function, passing the userID as an argument. This function initializes the passkey registration process and returns the createOptions object. The controller then sends the createOptions back to the client as a JSON response.

If the finish property is present, the controller function calls the finishServerPasskeyRegistration service function, passing the credential obtained from the request body. This function finalizes the passkey registration process on the server-side. Once the registration is complete, the controller sends a JSON response with a success message.

// controllers/passkey.js

import {
} from "../service/passkey.js";
import db from "../db.js";

async function handlePasskeyRegister(req, res) {
  const { user } = req;
  const userID =;

  if (!userID) {
    return res.status(401).json({ message: "Unauthorized" });
  console.log("userId", userID);

  const { start, finish, credential } = req.body;

  try {
    if (start) {
      const createOptions = await startServerPasskeyRegistration(userID);
      console.log("registration start");
      return res.json({ createOptions });
    if (finish) {
      await finishServerPasskeyRegistration(credential);
      return res.json({ message: "Registered Passkey" });
  } catch (error) {
    return res.status(500).json(error);

Now, we setup a /passkeys/register POST route. When a POST request is made to this, the handlePasskeyRegister controller function is invoked to handle the passkey registration process.

// routes/passkey.js

import express from "express";
const router = express.Router();
import { handlePasskeyRegister } from "../controllers/passkey.js";
import { checkAuth } from "../middleware/auth.js";"/passkeys/register", checkAuth, handlePasskeyRegister);

export default router;

Vue Frontend:

Make sure to install the @github/webauthn-json library in your frontend.

npm install @github/webauthn-json

Next, create a registerPasskey function that will be called when the user clicks a button to register a new passkey. It handles the client-side process of registering a passkey.

The function starts by sending a POST request to the server at the endpoint /api/passkeys/register to initiate the passkey registration process. The request includes a JSON payload with the following properties:

  • start: true: Indicates that the registration process is starting.
  • finish: false: Indicates that the registration process is not yet finished.
  • credential: null: Initially, no credential is provided.

The function then awaits the response from the server and extracts the createOptions from the response JSON.

Next, the function calls the create function from @github/webauthn-json library, passing the createOptions as an argument.

After obtaining the credential, the function sends another POST request to the same endpoint, but this time with a different payload:

  • start: false: Indicates that the registration process is no longer starting.
  • finish: true: Indicates that the registration process is finished.
  • credential: The newly created passkey credential.

Finally, if the response is successful, a passkey is registered for the user.

import { useRouter } from 'vue-router';
  import { create, type CredentialCreationOptionsJSON } from '@github/webauthn-json';

  const router = useRouter();
    const registerPasskey = async () => {
        const createOptionsResponse = await fetch("http://localhost:5001/api/passkeys/register", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          credentials: 'include',
          body: JSON.stringify({ start: true, finish: false, credential: null }),
    const { createOptions } = await createOptionsResponse.json();
    console.log("createOptions", createOptions);
    const credential = await create(createOptions as CredentialCreationOptionsJSON);
    const response = await fetch("http://localhost:5001/api/passkeys/register", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      credentials: "include",
      body: JSON.stringify({ start: false, finish: true, credential }),
    if (response.ok) {
        console.log("Passkey registered successfully");

Now, to make it work we're missing one crucial step: getting the Tenant ID and API key secret from Hanko Cloud. For that, navigate over to Hanko, create an account, and set up your organization. Navigate to 'Create new project', select 'Passkey Infrastructure', and provide your project name and URL.

Note: It's recommended to create separate projects for your development and production environments. This way, you can use your frontend localhost URL for the development project and your production URL for the live project, ensuring a smooth transition between environments without the need to modify the 'App URL' later.

Obtain your Tenant ID and create an API key, then add the Tenant ID and API key secret to your backend's '.env' file.

Now that you have your secrets added to your backend's .env file, you should be all set to register a passkey. Go ahead and give it a try!

Passkey login


To enable passkey login, we'll create two more service functions to initiate and finish the login process.

// services/passkey.js

async function startServerPasskeyLogin() {
  const options = await passkeyApi.login.initialize();
  return options;

async function finishServerPasskeyLogin(options) {
  const response = await passkeyApi.login.finalize(options);
  return response;

Now, similar to handlePasskeyRegister create a handlePasskeyLogin controller.

This function handles process of logging in with a passkey. It receives a request with start, finish, and options properties. If start is true, it initiates the login process by calling startServerPasskeyLogin() and returns the login options. If finish is true, it completes the login process by calling finishServerPasskeyLogin() with the provided options. It then retrieves the user ID from the JWT token, finds the corresponding user in the database, creates a session ID, sets the user for the session, and logs them in.

// controllers/passkey.js

import {
} from "../service/passkey.js";
import { getUserID } from "../service/get-user-id.js";
import { v4 as uuidv4 } from "uuid";
import { setUser } from "../service/auth.js";
import db from "../db.js";

async function handlePasskeyLogin(req, res) {
  const { start, finish, options } = req.body;

  try {
    if (start) {
      const loginOptions = await startServerPasskeyLogin();
      return res.json({ loginOptions });
    if (finish) {
      const jwtToken = await finishServerPasskeyLogin(options);
      const userID = await getUserID(jwtToken?.token ?? "");
      console.log("userID from hanko", userID);
      const user = db.users.find((user) => === userID);
      if (!user) {
        return res.status(401).json({ message: "Invalid user" });
      console.log("user", user);
      const sessionId = uuidv4();
      setUser(sessionId, user);
      res.cookie("sessionId", sessionId);
      return res.json({ message: " Passkey Login successful" });
  } catch (error) {
    return res
      .json({ message: "An error occurred during the passkey login process." });

In the /passkeys/login POST route, we invoke the above controller.

// routes/passkey.js"/passkeys/login", handlePasskeyLogin);


Now we create a signInWithPasskey function. This function will interact with the backend APIs to initiate and finalize the passkey login process.

async signInWithPasskey() {
      try {
        const createOptionsResponse = await fetch("http://localhost:5001/api/passkeys/login", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          credentials: 'include',
          body: JSON.stringify({ start: true, finish: false, credential: null }),

        const { loginOptions } = await createOptionsResponse.json();

        // Open "sign in with a passkey" dialog
        const options = await get(loginOptions);

        const response = await fetch("http://localhost:5001/api/passkeys/login", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          credentials: 'include',
          body: JSON.stringify({ start: false, finish: true, options }),

        if (response.ok) {
          this.$router.push({ name: 'dashboard' });
        } else {
          throw new Error("Login with a passkey failed");
      } catch (error) {
        console.error("Login with a passkey error:", error);
        // Optionally, handle error (e.g., show an error message to the user)

Congratulations! You now have a fully functional passkey login in your Vue.js app, significantly enhancing the user experience and making the authentication process more seamless for your users 🚀 Feel free to reach out to us on Discord, if you get into any issues.

Github repo:

Back to overview

More blog posts

Don't miss out on latest blog posts, new releases and features of Hanko's products, and more.

Your submission has been received!
Something went wrong.