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
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
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
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>
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>
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)
}
}
}
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.
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 = '...'
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>
// 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
})
...
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>
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)
}
}
}
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)