Passkeys in Sveltekit with AuthJS

May 20, 2024

Passkeys in Sveltekit with AuthJS

Table of Contents

Introduction

Auth.js has recently added support for Passkeys via WebAuthn specification. However, at the time of this writing, the Auth.js guide does not have a working example of how to implement passkeys in Sveltekit. The current official guide is for Next.js. It uses the Passkey provider, which is not compatible with the Sveltekit implementation. For Sveltekit, you will need to use the WebAuthn provider, which is not documented.

This guide will walk you through the process of setting up passkeys in Sveltekit using Auth.js. In addition, it also includes instructions on supporting multiple passkeys. For this guide, I’ll also be using PostgreSQL and Drizzle as the Database and ORM of choice. There will be differences from the in the Auth.js documentation, as this guide will include steps for allowing users to add multiple passkeys.

1. Install peer dependencies

The @simplewebauthn/browser peer dependency is only required for custom signin pages. If you’re using the Auth.js default pages, you can skip installing that peer dependency.

bash
pnpm add @simplewebauthn/[email protected] @simplewebauthn/[email protected]

2. Setup the database table

I am using a slightly modified table definition from the Drizzle adapter to allow for multiple named passkeys for each user. The name column is added to the authenticator table, and a unique constraint is added to ensure that each user can only have one passkey with the same name.

In addition, you should add a unique index on the userId and providerAccountId columns of the account table. This is necessary to have a foreign key constraint pointing to these columns on the authenticator table.

3. Update Auth.js configuration

Add the WebAuthn provider to your configuration. You will need to have at least one other provider, because Passkey registration requires a user to be signed in.

4. Pass the registered authenticator entries to the UI

By passing the authenticators to the layout, we can display the list of registered passkeys to the user. It will be accessible in the built-in $page.data.authenticators store.

5. Create an endpoint to rename or delete a passkey

This API endpoint will be used in the next step to rename a passkey after it has been registered.

6. Add the passkey list and registration button

Registering a passkey requires the user to be signed in, so the registration should be somewhere in the user settings.

In this example, when the user clicks the “Add Passkey” button, the passkey function is called with the action set to register. This will trigger the registration flow. The redirect option is set to false to prevent the user from being redirected after the registration is complete.

Instead, the user will be directed to create a name for the new passkey using the renameWebAuthn function. After the rename flow is complete, the invalidateAll function is called to rerun the load functions that belong to the current page.

7. Add the sign in flow

The sign in flow is similar to the registration flow. You call the same passkey function with the action set to authenticate instead. The callbackUrl option is set to the URL of the page you want to redirect to after the sign in is complete.

svelte
<script lang="ts">
  import { page } from "$app/stores";
	import { signIn as passkey } from "@auth/sveltekit/webauthn";
</script>

<button
  class="flex h-16 items-center gap-4 rounded-lg bg-base-200 px-8 py-4 text-base-content transition-colors hover:bg-base-300"
  on:click={() =>
    passkey("webauthn", {
      callbackUrl: $page.url.searchParams.get("redirect") || "/characters",
      action: "authenticate"
    })}
  aria-label="Sign in with Passkey"
>
  <span class="iconify h-8 w-8 material-symbols--passkey"></span>
  <span class="flex h-full flex-1 items-center justify-center text-xl font-semibold">Sign In with Passkey</span>
</button>
<span class="max-w-72 text-balance text-center text-xs text-base-content">
  You must create an account and enable Passkey in settings before you can sign in with this.
</span>

Making the passkey sign in flow automatic

If you wish to have the sign in flow automatically initiate when the login page is loaded, you can call the passkey function programatically. If you do this, it would be a good idea to set a localStorage variable when the user is signed in if they have registered a passkey. Then on the login page only initiate the sign in flow if the user has a passkey registered.

Using the same global layout from step 4, you can create such a local variable.

Then on your login page, initiate the sign in flow only if the local storage variable is set.