Clerk with Authn (PassKeys) in NextJs
Published: 2024-03-11 03:06:04


If you're like me, you already love using [Clerk](https://clerk.com/) to handle security in your App.

However, one cool feature that is missing is the Authn/Passkey integration. Basically being able to login with your FaceId or Fingerprint. Which is pretty cool.

Since it is [not coming soon](https://github.com/orgs/clerk/discussions/1099) to Clerk, I'm gonna show you how to hack a solution by yourself.


## Setup a new Clerk Project

Go to your Clerk Dashboard and create a new [project](https://clerk.com/docs/quickstarts/setup-clerk). I'm gonna call mine Passki (yes very creative) and select only the username for login (I'll later use this username as an unique identifier so keep this in mind).


![New Clerk Project Design](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ljs2pvy7f4c8mhfyfqqr.png)


## Start a new NextJs Project

Lets go with the default settings and call my app Passki. I'll be using NextJs14 for this (currently NextJs' latest version).

```
What is your project named? my-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like to use `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to customize the default import alias (@/*)? No / Yes
What import alias would you like configured? @/*
```

## Setup Clerk inside NextJs

Lets follow the steps in [here](https://clerk.com/docs/quickstarts/nextjs) to QuickStart our Clerk setup.

By now you should've done these from the QuickStart tutorial

1. Add your env variables
2. Wrap the app with your ClerkProvided
3. Added the `middleware.ts`
4. Added a simple `<UserButton />` in your main App Page to login and logout

At the end of this blog post you can find the Repo for the project :)

Now to the fun part.


## Strategize a solution

### Understanding Authn

Authn is a new way of authenticating. Instead of relying on remembering your password you can login using a biometric, like your face or fingerprint. 

This leads to a better User Experience since remembering passwords sucks.

![Authn diagram how it works](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yklxmhvf3bpow8s7s6wo.png)

### How WebAuthn Works:

1. **Frontend requests a Challenge:** When you try to log in, you ask the server for a unique challenge, which is essentially a random, one-time code.

2. **Activate Your Biometric:** You then use your biometric (like a fingerprint or face scan) to respond. This action activates the private key stored securely on your device.

3. **Private Key Signs the Challenge:** The private key, unlocked by your biometric, signs this challenge, turning it into a 'signed challenge.'

4. **Send Signed Challenge Back to the Server:** Your device sends this signed challenge back to the server.

5. **Server Verifies with Public Key:** The server uses the public key, which corresponds to your private key, to verify the signed challenge. If it matches, your identity is confirmed, and you are granted access.


### How to approach Authn with code

First we will setup the necessary libraries for this project.

Next, we will register our fingerprint following the 5 steps above.

Lastly, we will sign in with the registered fingerprint also following the 5 steps from above.

Lets get to it!


## Setup

We'll be using the `@simplewebauthn` libraries for the whole project. I've tested many libraries to implement Authn and this on is very straightforward and intuitive.

Go ahead and install these

```
npm install @simplewebauthn/server
```

```
npm install @simplewebauthn/browser
```

```
npm install @simplewebauthn/types
```

We will also add our main entity PassKey. I will create a new path called `/features/types.ts` to save it for later use

```ts
export type PassKey = {
  credentialId: Uint8Array;
  credentialPublicKey: Uint8Array;
  counter: number;
  credentialDeviceType: string;
  credentialBackedUp: boolean;
  transports: Transport[];
  createdTs: number;
};

export type Transport =
  | 'ble'
  | 'cable'
  | 'hybrid'
  | 'internal'
  | 'nfc'
  | 'smart-card'
  | 'usb';
```

This PassKey Type will simplify our pipeline since it is the bridge between our logic and the `@simplewebauthn` package.

We will also need to add some identifier constants so that Authn knows which are our authorized initializers. Let's save them under `/features/constants`

```ts
"user server"

// Human-readable title for your website
export const RP_NAME = 'Passki';

// A unique identifier for your website
export const RP_ID = 'localhost';

// The URL at which registrations and authentications should occur
export const RP_ORIGIN = 'http://localhost:3000';
```

Lastly, this whole Authn pipeline requires us to keep track of an user's passkeys and their latest challenge (to prevent replay attacks). You would normally use a Database for that. But for the sake of this tutorial we are gonna use Clerk's inbuilt feature User's metadata.

The User's metadata allows us to save and access extra stuff from the User, which works really well for this tutorial like the PassKeys and the latest challenge. 

In this case, we will use the private metadata since that can't be accessed from the frontend (only backend if you separate your environments well in NextJs).

Please note that an User's metadata is capped at 4kb, so better not abuse it haha.


## Register your fingerprint

We should only allow registered users to add their fingerprint for extra security. Go ahead and add the [custom Sign Up](https://clerk.com/docs/references/nextjs/custom-signup-signin-pages#build-your-own-sign-in-and-sign-up-pages-for-your-next-js-app-with-clerk).

Create a new Sign Up page here `app/sign-up/[[...sign-up]]/page.tsx` and also add this env variable
`NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up`

The `page.tsx` component should look something like this

```tsx
import { SignUp } from "@clerk/nextjs";

export default function SignUpPage() {
  return (
    <div className="h-screen flex items-center justify-center">
      <SignUp />
    </div>
  );
}
```

Good. That will allow us to sign up for the first time. Now let's add a new fingerprint for a User.

Let's create a new path called `/features/register/` where we can create the server functions necessary to serve our Components.

Next, create a createRegisterOptions function which will generate Registration Options (including a new Challenge) for the user to register a new PassKey. I'll save this new function in `features/register/create-registration-options.ts`

```ts
'use server';
import { clerkClient, currentUser } from '@clerk/nextjs/server';
import { generateRegistrationOptions } from '@simplewebauthn/server';
import { PublicKeyCredentialCreationOptionsJSON } from '@simplewebauthn/types';
import { PassKey } from '@/features/types';
import { RP_ID, RP_NAME } from '@/features/constants';

export async function createRegistrationOptions(): Promise<PublicKeyCredentialCreationOptionsJSON> {
  const user = await currentUser();
  if (!user || !user.username) {
    throw new Error('No user found');
  }
  // Generate WebAuthn registration options
  const userMetadata = user.privateMetadata;
  const userPassKeys = userMetadata.passKeys as PassKey[] || [];
  const options = await generateRegistrationOptions({
    rpName: RP_NAME,
    rpID: RP_ID,
    userID: user.id,
    userName: user.username,
    attestationType: 'none',
    authenticatorSelection: {
      residentKey: 'preferred',
      userVerification: 'preferred',
      authenticatorAttachment: 'platform',
    },
    excludeCredentials: userPassKeys.map((passKey) => ({
      id: passKey.credentialId,
      type: 'public-key',
    })),
  });

  // Update the latest challenge to prevent replay attacks
  await clerkClient.users.updateUserMetadata(user.id, {
    privateMetadata:{
      ...userMetadata,
      latestPassKeyChallenge: options.challenge,
    }
  });
  return options
}

```

With those registerOptions, our frontend Component will begin the process to register a new Passkey. It will then call the `startRegistration` function and send it to validate to our `endRegistration` function. Let's also add the `endRegistration`function.

```ts
'use server';

import { verifyRegistrationResponse } from '@simplewebauthn/server';
import { RP_ID, RP_ORIGIN } from '@/features/constants';
import { RegistrationResponseJSON } from '@simplewebauthn/types';
import { PassKey } from '../types';
import { clerkClient, currentUser } from '@clerk/nextjs';

export async function endRegistration(registrationResponseJSON: RegistrationResponseJSON) {
    const user = await currentUser();
    const userMetadata = user?.privateMetadata
    if (!user || !userMetadata) {
      throw new Error('No user found');
    }
    const expectedChallenge = userMetadata.latestPassKeyChallenge as string || ''
    try {
      const verification = await verifyRegistrationResponse({
        response: registrationResponseJSON,
        expectedChallenge,
        expectedOrigin: RP_ORIGIN,
        expectedRPID: RP_ID,
      });
      const { verified, registrationInfo } = verification;
      if (verified && registrationInfo) {
        console.log('Registration verified');
        const {
          credentialPublicKey,
          credentialID,
          counter,
          credentialBackedUp,
        } = registrationInfo;
        const foundTransports = registrationResponseJSON.response.transports;
        const newPassKey:PassKey = {
          credentialId: credentialID,
          credentialPublicKey: credentialPublicKey,
          counter,
          credentialDeviceType: 'singleDevice',
          credentialBackedUp,
          transports: foundTransports || [],
            createdTs: Date.now(),
        }
        // Save the new pass key to the user's private metadata
        const userPassKeys = userMetadata.passKeys as PassKey[] || [];
        await clerkClient.users.updateUserMetadata(user.id, {
          privateMetadata:{
            ...userMetadata,
            passKeys: [...userPassKeys, newPassKey],
          }
        });
        return true;
      } else {
        return false;
      }
    } catch (error) {
      console.error(error);
      return false;
    }
  }
````

Let's create a new page called `app/register-pass-key/page.tsx` where we will first call the `createRegistrationOptions` function, then `startRegistration` and lastly call the `endRegistration`function.

Here is a pretty Component using some Tailwind for that.

````ts
"use client";
import { useCallback } from "react";
import { createRegistrationOptions } from "@/features/register/create-registration-options";
import { startRegistration } from "@simplewebauthn/browser";
import { endRegistration } from "@/features/register/end-registration";

export default function RegisterPassKey() {
  const onClick = useCallback(async () => {
    const registrationOptions = await createRegistrationOptions();
    console.log("registrationOptions", registrationOptions);
    try {
      const registrationResponse = await startRegistration(registrationOptions);
      console.log("registrationResponse", registrationResponse);
      const registrationStatus = await endRegistration(registrationResponse);
      console.log("Registration successful", registrationStatus);
    } catch (error: unknown) {
      alert("Error: Authenticator was probably already registered by user");
      console.error("Error starting registration:", error);
      return;
    }
  }, []);

  return (
    <div className="relative isolate flex h-screen w-screen items-center justify-center overflow-hidden bg-gray-900">
      <div className="px-6 py-24 sm:px-6 sm:py-32 lg:px-8">
        <div className="mx-auto max-w-2xl text-center">
          <h2 className="text-4xl font-bold tracking-tight text-white sm:text-4xl">
            Setup your own Passkey
            <br />
          </h2>
          <p className="mx-auto mt-6 max-w-xl text-2xl leading-8 text-gray-300">
            You can use your fingerprint or FaceId to login to your account
          </p>
          <div className="mt-10 flex items-center justify-center gap-x-6">
            <button
              onClick={onClick}
              className="rounded-md bg-white px-3.5 py-2.5 text-sm font-semibold text-gray-900 shadow-sm hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
            >
              Lets do it!
            </button>
          </div>
        </div>
      </div>
      <svg
        viewBox="0 0 1024 1024"
        className="absolute left-1/2 top-1/2 -z-10 h-[64rem] w-[64rem] -translate-x-1/2 [mask-image:radial-gradient(closest-side,white,transparent)]"
        aria-hidden="true"
      >
        <circle
          cx={512}
          cy={512}
          r={512}
          fill="url(#8d958450-c69f-4251-94bc-4e091a323369)"
          fillOpacity="0.7"
        />
        <defs>
          <radialGradient id="8d958450-c69f-4251-94bc-4e091a323369">
            <stop stopColor="#7775D6" />
            <stop offset={1} stopColor="#E935C1" />
          </radialGradient>
        </defs>
      </svg>
    </div>
  );
}

```

Right now if you sign in and go to the `register-pass-key`page you should be able to add your fingerprint to your Clerk account.


![Add Fingerprint screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/27kh2w8qdae1proobzhs.png)


![Register Fingerprint Modal](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x1b2c6gw26hip1vfgtip.png)

If we go to our Clerk Dashboard we can see that indeed the LatestChallenge (now obsolete but still there) and the `passKeys`are there (only 1 for now). 


![Clerk User's Metadata](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s5v97il7a0hlivxuosjo.png)

Great job!

Now, this was the easy part haha. We will now turn to the second part. Actually using your fingerprint to sign in.

## Authentication

Good. As we did before let's create our Sign In Component.

Add to your `.env.local` this env variable
`NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in`

And create the component inside `app/sign-in/[[...sign-in]]/page.tsx`

```ts
"use client";
import { useState } from "react";
import { SignIn } from "@clerk/nextjs";
import SignInPassKey from "@/features/authenticate/components/signInPassKey";

export default function SignInPage() {
  const [newScreen, setNewScreen] = useState(false);
  
  return (
    <>
      {newScreen ? (
        <SignInPassKey />
      ) : (
        <div className="h-screen flex flex-col items-center justify-center">
          <SignIn />
          <button
            onClick={() => setNewScreen(true)}
            type="button"
            className="mt-3 rounded-md bg-blue-700 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-900 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-700"
          >
            Log in with my fingerprint ✨
          </button>
        </div>
      )}
    </>
  );
}
```

This Component has a child Component with our Authentication with Authn logic `<SignInPassKey />`. We will come back to this Component after we create the aux functions (aka the server functions).


Once again, we start by obtaining the **authentication options ** to generate a New Challenge and other params.

In this case, we take the username as an input since we also need to update the user's metadata to the latest challenge (for extra security) and the username is the way we can identify the User.

Here is the code for that

```ts
'use server';
import { clerkClient } from '@clerk/nextjs/server';
import { generateAuthenticationOptions } from '@simplewebauthn/server';
import { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types';
import { RP_ID } from '@/features/constants';

export async function createAuthenticationOptions(username:string): Promise<PublicKeyCredentialRequestOptionsJSON> {
  console.log('username input', username);
  if (!username) {
    throw new Error('No username provided for authentication options.');
  }
  const usernames = [username]

  const possibleUsers = await clerkClient.users.getUserList({ username:usernames });
  // A reasonable assumption since usernames should be unique
  const user = possibleUsers[0]

  // Generate WebAuthn authentication options
  const userMetadata = user.privateMetadata;
  const options = await generateAuthenticationOptions({
    rpID: RP_ID,
    userVerification: 'preferred',
  });

  // Update the latest challenge to prevent replay attacks
  await clerkClient.users.updateUserMetadata(user.id, {
    privateMetadata:{
      ...userMetadata,
      latestPassKeyChallenge: options.challenge,
    }
  });
  return options
}

```

With those options, the frontend Component will run the Authentication pipeline and return to our Server the Authentication Response to validate it with `endAuthentication`. 

Here is `endAuthentication`

```ts
"use server";
import { clerkClient } from "@clerk/nextjs";
import {
  AuthenticationResponseJSON,
  AuthenticatorDevice,
} from "@simplewebauthn/types";
import {
  base64URLStringToUint8Array,
  findPassKeyByCredentialId,
} from "./utils";
import { verifyAuthenticationResponse } from "@simplewebauthn/server";
import { RP_ID, RP_ORIGIN } from "../constants";
import { PassKey } from "../types";
import { createSignInToken } from "./create-sign-in-token";

export async function endAuthentication(
  authenticationResponseJSON: AuthenticationResponseJSON
) {
  const { id, response } = authenticationResponseJSON;
  const { userHandle } = response; // This is the user ID
  console.log("userHandle", userHandle);
  if (!userHandle) return;
  const user = await clerkClient.users.getUser(userHandle);
  const expectedChallenge =
    (user.privateMetadata.latestPassKeyChallenge as string) || "";
  const credentialId = base64URLStringToUint8Array(id);
  const userPassKeys = (user.privateMetadata.passKeys as PassKey[]) || [];
  if (!userPassKeys) return;
  const passKey = findPassKeyByCredentialId(credentialId, userPassKeys);
  if (!passKey) return "";
  const credentialPublicKey = passKey.credentialPublicKey as Uint8Array;
  const credentialID = passKey.credentialId;
  // You may have issues with this authenticator
  // by getting "no data" make sure your types are correct.
  // That is why the extra casting
  const authenticator: AuthenticatorDevice = {
    credentialPublicKey: new Uint8Array(Object.values(credentialPublicKey)),
    credentialID: new Uint8Array(Object.values(credentialID)),
    counter: passKey.counter,
    transports: passKey.transports,
  };

  try {
    const verification = await verifyAuthenticationResponse({
      response: authenticationResponseJSON,
      expectedChallenge,
      expectedOrigin: RP_ORIGIN,
      expectedRPID: RP_ID,
      authenticator,
    });
    console.log("verification response", verification);
    if (verification.verified) {
      const newSignInToken = await createSignInToken(user.id);
      return newSignInToken;
    }
  } catch (error) {
    console.error(error);
    return;
  }
}
```

There are extra utils functions that help us with the parsing of Uint8's and making sure the data remains consisten accross the function. 

In the end, you can see that once the Verification is Verified, we generate a [Sign In Token with Clerk](https://clerk.com/docs/reference/backend-api/tag/Sign-in-Tokens). 

These Sign In Tokens are one time tokens to Sign In the App so they come in very handy for implementing Authn securely.

All right. now we have everything we need. Let's integrate it in the frontend Component

```ts
"use client";
import { useCallback, useState } from "react";
import { createAuthenticationOptions } from "@/features/authenticate/create-authentication-options";
import { startAuthentication } from "@simplewebauthn/browser";
import { endAuthentication } from "@/features/authenticate/end-authentication";
import { useSignIn } from "@clerk/nextjs";

export default function SignInPassKey() {
  const { signIn, setActive } = useSignIn();
  const [username, setUsername] = useState("");

  const passkeyAuthenticate = useCallback(async () => {
    const authenticationOptions = await createAuthenticationOptions(username);
    try {
      const authenticationResponse = await startAuthentication(
        authenticationOptions,
        false
      );
      console.log("authenticationResponse", authenticationResponse);
      const signInToken = await endAuthentication(authenticationResponse);
      console.log("sign in token", signInToken);
      if (!signIn || !signInToken) return;
      const res = await signIn.create({
        strategy: "ticket",
        ticket: signInToken as string,
      });
      console.log("auth response", res);
      setActive({
        session: res.createdSessionId,
      });
    } catch (error: unknown) {
      console.error("Error starting authentication:", error);
      return;
    }
  }, [username, setActive, signIn]);

  return (
    <div className="h-screen flex flex-col items-center justify-center">
      <div>
        <h2 className="text-4xl font-bold tracking-tight text-white sm:text-4xl">
          Log In with your PassKey
        </h2>
        <div className="justify-self-start mt-2">
          <label
            htmlFor="username"
            className="block text-sm font-medium leading-6 text-white text-2xl"
          >
            Username
          </label>
          <div className="mt-2 w-full">
            <input
              id="username"
              type="text"
              value={username}
              onChange={(e) => setUsername(e.target.value)}
              className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
            />
          </div>
          <button
            type="button"
            onClick={passkeyAuthenticate}
            className="mt-3 rounded-md  w-full bg-blue-700 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-900 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-700"
          >
            Log in with my fingerprint ✨
          </button>
        </div>
      </div>
    </div>
  );
}
```

And there you have it! Lets try it out!

We go to the PassKey section


![Login With PassKey](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ckxcbwfy7oiuz4nwxz73.png)


We choose our PassKey

![Select PassKey from Auth Modal](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oouxr3nxkq9u9jgqstf1.png)

And we Sign In!!!


![Use your fingerprint](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rzg58ldu0po6gft17ceo.png)

And just like that. We are signed in with our Fingerprint 


![Signed in screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6g9euagi9c4rnrfv4vfx.png)


## Next up

This was a lot of work. But we made it possible :)

If you have any comments or doubts, I'd be happy to discuss them on [X](https://twitter.com/RobertoYamanaka) with you.

Thanks for reading. In the next post I'll show you how to setup Clerk Phone SignUp in Expo React Native. It will be fun.

Until next time!
Roberto

[Github Repo](https://github.com/robertoyamanaka/passki)