DEV Community

Cover image for How to add passkey login to your SvelteKit app
Toby Hobson for Passlock

Posted on • Edited on • Originally published at passlock.dev

How to add passkey login to your SvelteKit app

In this tutorial you'll learn how to add passkey authentication to your SvelteKit apps. In subsequent tutorials I'll show you how to add session management, social login and more.

Check out the full SvelteKit + Authentication Tutorial on my blog.

Feeling lazy?

I've put together a SvelteKit Starter App which supports passkeys, social sign in and other features. You can choose from multiple UI frameworks including Daisy UI, Preline and Shadcn. Use the CLI script and follow the prompts:

pnpm create @passlock/sveltekit
Enter fullscreen mode Exit fullscreen mode

That's it! check out the generated source code, which has plenty of comments. If you like it, please give it a star 🙏

Alternatively read on to learn how to do this manually...

Create a SvelteKit app

Use SvelteKit's CLI to generate a skeleton project:

pnpm create svelte@latest my-app
Enter fullscreen mode Exit fullscreen mode

Note: Choose the skeleton project template, with Typescript support.

Add the library

We'll use Passlock, my SvelteKit library for passkey registration and authentication:

pnpm add -D @passlock/sveltekit
Enter fullscreen mode Exit fullscreen mode

Create a registration route

Create a new template at src/routes/register/+page.svelte:

<!-- src/routes/register/+page.svelte -->
<form method="post">
  Email: <input type="text" name="email" /> <br />
  First name: <input type="text" name="givenName" /> <br />
  Last name: <input type="text" name="familyName" /> <br />
  <button type="submit">Register</button>
</form>
Enter fullscreen mode Exit fullscreen mode

This won’t win any design awards but I want to keep things simple. Now for the real work … we’ll intercept the form submission and register a passkey on the users device:

<!-- src/routes/register/+page.svelte -->
<script lang="ts">
  import { enhance } from '$app/forms'
  import { register } from '@passlock/sveltekit'

  // we'll fill in the tenancyId and clientId later
  const { onSubmit } = register({ tenancyId: 'TBC', clientId: 'TBC' })
</script>

<form method="post" use:enhance={onSubmit}>
  Email: <input type="text" name="email" /> <br />
  First name: <input type="text" name="givenName" /> <br />
  Last name: <input type="text" name="familyName" /> <br />
  <button type="submit">Register</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Explanation

We’re using SvelteKit’s progressive enhancement to intercept the form submission. We use Passlock’s SvelteKit extension to register a passkey.

If all goes well, this will return a token that we can exchange for a Principal in our form action.

Process the token in the form action

The form will be submitted with an additional token field. We’ll use it to fetch the passkey details in the form action.

Create a new form action at src/routes/register/+page.server.ts:

// src/routes/register/+page.server.ts
import type { Actions } from './$types'
import { Passlock, TokenVerifier } from '@passlock/sveltekit'

// we'll replace the TBCs later
const tokenVerifier = new TokenVerifier({
  tenancyId: 'TBC',
  apiKey: 'TBC'
})

export const actions: Actions = {
  default: async ({ request }) => {
    const formData = await request.formData()
    const token = formData.get('token') as string
    const result = await tokenVerifier.exchangeToken(token)

    if (Passlock.isPrincipal(result)) {
      console.log(result)
    } else {
      console.error(result.message)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The result includes a sub field (subject / user id), along with an authId (authenticator i.e. passkey id). In the next tutorial we'll use this to link the passkey to a local user account.

Create a Passlock cloud account

We've used TBC for some Passlock config values. It's time to replace these with real values.

Create a developer account at passlock.dev then head to the settings tab within your console. We're after the tenancyId, clientId and apiKey values.

Note: Passlock cloud is a serverless passkey platform that also supports social login, mailbox verification, audit logs and more. It's free for personal and commercial use.

Find your Passlock config

Edit your .env file (or .env.local) and create entries for these values:

# .env
PUBLIC_PASSLOCK_TENANCY_ID = '...'
PUBLIC_PASSLOCK_CLIENT_ID = '...'
PASSLOCK_API_KEY = '...'
Enter fullscreen mode Exit fullscreen mode

If you don't have a .env file in your app root, create one.

You can now reference these in your template and form actions:

<!-- src/routes/register/+page.svelte -->
<script lang="ts">
  import {
    PUBLIC_PASSLOCK_TENANCY_ID,
    PUBLIC_PASSLOCK_CLIENT_ID
  } from '$env/static/public'

  const { onSubmit } = register({ 
    tenancyId: PUBLIC_PASSLOCK_TENANCY_ID, 
    clientId: PUBLIC_PASSLOCK_CLIENT_ID, 
  })

  ...
</script>
Enter fullscreen mode Exit fullscreen mode
// src/routes/register/+page.server.ts
import { PUBLIC_PASSLOCK_TENANCY_ID } from '$env/static/public'
import { PASSLOCK_API_KEY } from '$env/static/private'

const tokenVerifier = new TokenVerifier({
  tenancyId: PUBLIC_PASSLOCK_TENANCY_ID,
  apiKey: PASSLOCK_API_KEY
})

...
Enter fullscreen mode Exit fullscreen mode

Try to register a passkey

Although we're not yet finished, you should be at a point where you can register a passkey.

Navigate to the /register page and complete the form. You should be prompted to create a passkey and the form action will spit out details of the passkey registration.

You should also see an entry in the users tab of your Passlock console. The console can be used to view security related events, suspend and delete users and more.

Too slow?

At this stage things may seem slow and clunky. We'll address this in a subsequent tutorial, for now we just want to get things working.

Create a login route

We can now register a passkey on the users device. Let’s use that passkey to authenticate. The process is essentially the same as it was for registration.

Create a login template at src/routes/login/+page.svelte:

<!-- src/routes/login/+page.svelte -->
<script lang="ts">
  import { enhance } from '$app/forms'
  import { login } from '@passlock/sveltekit'

  import {
    PUBLIC_PASSLOCK_TENANCY_ID,
    PUBLIC_PASSLOCK_CLIENT_ID
  } from '$env/static/public'

  const { onSubmit } = login({ 
    tenancyId: PUBLIC_PASSLOCK_TENANCY_ID, 
    clientId: PUBLIC_PASSLOCK_CLIENT_ID
  })
</script>

<form method="post" use:enhance={onSubmit}>
  Email:
  <input type="text" name="email" />
  <br />

  <button type="submit">Login</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Notice how we’re only passing the email this time. Now for the form action:

// src/routes/login/+page.server.ts
import type { Actions } from './$types'
import { Passlock, TokenVerifier } from '@passlock/sveltekit'
import { PUBLIC_PASSLOCK_TENANCY_ID } from '$env/static/public'
import { PASSLOCK_API_KEY } from '$env/static/private'

const tokenVerifier = new TokenVerifier({
  tenancyId: PUBLIC_PASSLOCK_TENANCY_ID,
  apiKey: PASSLOCK_API_KEY
})

export const actions: Actions = {
  default: async ({ request }) => {
    const formData = await request.formData()
    const token = formData.get('token') as string
    const result = await tokenVerifier.exchangeToken(token)

    if (Passlock.isPrincipal(result)) {
      console.log(result)
    } else {
      console.error(result.message)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The form action is the same (at this stage). Passlock abstracts passkey registration and authentication into a common structure known as a Principal.

Try to login using your passkey

Navigate to the /login page, enter your email (the one you used for registration) and click login. If all goes well, you should see your user details in the server console.

Within your Passlock console, under the users tab you should see an entry. If you click on the user you'll be able to see the passkey registration and authentication events.

Summary

We used the Passlock library to register a passkey on the users device. The public key component of the passkey is stored in your Passlock vault.

During authentication we ask the user to present their passkey. Passlock verifies that the passkey is authentic, then generates a secure token. This token is passed to the backend form action in the form submission.

The form action verifies the secure token is authentic, exchanging it for details about the user and the passkey they used to authenticate.

Next steps

We've made a great start but we now need to link the passkeys to local user accounts and sessions. That's the subject of the next tutorial..

Top comments (0)