Guide
6 Min.
Read

How to add passkeys to your Next.js application

If you're looking to add Passkey Login to your existing Next.js app, you're in the right place! Passkey login is a secure and user-friendly authentication method that eliminates the need for traditional passwords. By implementing passkeys, you can provide your users with a seamless and secure login experience.

In this tutorial, we'll walk you through the step-by-step process of adding a passkey login into your Next.js app using the Hanko Passkey API. We'll assume that you already have a Next.js app with authentication set up, so we won't focus on that here. Instead, we'll dive straight into adding Passkeys to your existing auth system.

If you're already using NextAuth for authentication in your Next.js app, we've got you covered! Check out our guide on integrating passkey login using our NextAuth Passkey Provider.

So, let's get started on upgrading your app's login experience! To make it easier for you to follow along, we have created a sample app that you can use as a reference throughout the tutorial.

We'll be implementing passkey functionality in two phases.

  • Passkey Registration: In this phase, we'll set up the necessary things to allow users to register and store their passkey.
  • Sign In with Passkey: Once users have registered their passkeys, we'll implement the functionality to enable them to log in using their registered passkeys.

Passkey Registration

Make sure you have the passkey SDK installed.

npm i @teamhanko/passkeys-sdk

To enable passkey registration, we define two server actions: startServerPasskeyRegistration and finishServerPasskeyRegistration.

The startServerPasskeyRegistration function retrieves the user session, extracts the user ID and email, and initializes the passkey registration process using the passkeyApi.registration.initialize method from the SDK. It returns the registration options to the client.

The finishServerPasskeyRegistration function finalizes the passkey registration by calling passkeyApi.registration.finalize with the provided credentials.

// lib/passkey.ts

"use server";

import { tenant } from "@teamhanko/passkeys-sdk";
import { getSession } from "./auth";

const passkeyApi = tenant({
  apiKey: process.env.PASSKEYS_API_KEY!,
  tenantId: process.env.PASSKEYS_TENANT_ID!,
});

export async function startServerPasskeyRegistration() {
  const session = await getSession();
  console.log("session", session);
  const sessionUser = session?.user;

  const createOptions = await passkeyApi.registration.initialize({
    userId: sessionUser!.id,
    username: sessionUser!.email || "",
  });

  return createOptions;
}

export async function finishServerPasskeyRegistration(credential: any) {
  const session = await getSession();
  if (!session) throw new Error("Not logged in");
  await passkeyApi.registration.finalize(credential);
}

Now, it's time to implement passkey registration on the frontend. For that, we define an  registerPasskey function that will be triggered when the user clicks a button to register a new passkey.

// components/register-new-passkey.tsx

"use client"

import { finishServerPasskeyRegistration, startServerPasskeyRegistration } from '@/lib/passkey';
import { Button } from './ui/button'
import {
    create,
    type CredentialCreationOptionsJSON,
} from "@github/webauthn-json";

const RegisterNewPasskey = () => {
    async function registerPasskey() {
        const createOptions = await startServerPasskeyRegistration();
        const credential = await create(createOptions as CredentialCreationOptionsJSON);
        await finishServerPasskeyRegistration(credential);
    }
    return (
           <Button
               onClick={() => registerPasskey()}
               className="flex justify-center items-center space-x-2"
           >
               Register a new passkey
           </Button>
    )
}

export default RegisterNewPasskey

Let's break down the steps involved in the registerPasskey function:

1. We call the startServerPasskeyRegistration server action to initialize the passkey registration process. This function returns the registration options needed to create a new passkey credential.

2. We use the create function from the @github/webauthn-json library to create a new passkey credential based on the registration options received from the server. The create function prompts the user to authenticate using their device's passkey mechanism (e.g., Touch ID, Face ID, or Windows Hello).

3. Once the user successfully authenticates and creates the passkey credential, we call the finishServerPasskeyRegistration server action, passing the newly created credential as an argument. This function finalizes the passkey registration process on the server-side.

Get credentials from Hanko Cloud

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

We'll need to create two more server actions. The startServerPasskeyLogin function initializes the passkey login process by calling passkeyApi.login.initialize() and returns the login options to the client.

The finishServerPasskeyLogin function finalizes the passkey login by calling passkeyApi.login.finalize() with the provided options and returns the login response.

// lib/passkey.ts

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

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

Now, similar to passkey registration, inside the LoginPasskey component, we define a signInWithPasskey function, that will be triggered when the user clicks the "Sign in with a passkey" button.

// components/sign-in-with-passkey.tsx

"use client"

import { finishServerPasskeyLogin, startServerPasskeyLogin } from "@/lib/passkey";
import { get } from "@github/webauthn-json";
import { Button } from "./ui/button"
import { getUserID, loginWithPasskey } from "@/lib/auth";
import { redirect, useRouter } from "next/navigation";

export default function LoginPasskey() {
    const router = useRouter()
    async function signIn() {
        const assertion = await startServerPasskeyLogin();
        const credential = await get(assertion as any);
        const response = await finishServerPasskeyLogin(credential);
        if (!response || !response.token) {
            return null;
        }
        const { token } = response;
        const userID = await getUserID(token);
        if (!userID) {
            return null;
        }
        await loginWithPasskey(userID);
        router.push('/dashboard')
    }
    return (
           <Button
               onClick={() => signIn()}
               className="flex justify-center items-center space-x-2"
           >
               Sign in with a passkey
           </Button>
    )
}

Let's break down the steps involved in the signInWithPasskey function:

1. We call the startServerPasskeyLogin server action to initialize the passkey login process. This function returns the assertion options needed to retrieve the passkey credential.

2. We use the get function from the @github/webauthn-json library to retrieve the passkey credential based on the assertion options received from the server. The get function prompts the user to authenticate using their device's passkey mechanism.

3. Once the user successfully authenticates, we call the finishServerPasskeyLogin server action, passing the retrieved credential as an argument. This function finalizes the passkey login process on the server-side and returns the login response.

4. We check if the login response contains a valid token. If not, we return null to indicate a failed login attempt.

5. If the login response contains a valid token, we extract the token and use the getUserID function from @/lib/auth to retrieve the user ID associated with the token.

6. If the user ID is not found, we return null to indicate a failed login attempt.

7. If the user ID is successfully retrieved, we call the loginWithPasskey function, passing the user ID as an argument. This function checks if user with the provided id exists, then creates the session for the user.

// lib/auth.ts

export async function loginWithPasskey(userId: string) {
  const user = db.users.find((user) => user.id === userId);
  if (!user) {
    throw new Error("Authentication failed");
  }

  const sessionUser = { id: user.id, email: user.email };
  const expires = new Date(Date.now() + 24 * 60 * 60 * 1000);
  const session = await encrypt({ user: sessionUser, expires });

  cookies().set("session", session, { expires });
}

Thats it! You now have a fully functional passkey login in your Next.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: https://github.com/teamhanko/passkeys-nextjs

arrow
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.