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 + Passkeys tutorial on my blog.
Quick start
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.
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 passkey 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 real simple. Next, create a placeholder form action at src/routes/register/+page.server.ts
:
// src/routes/register/+page.server.ts
import type { Actions } from './$types'
export const actions: Actions = {
default: async () => {
// TODO
}
}
Now for the real work... update the template so it intercepts the form submission and registers a passkey on the user's device:
<!-- src/routes/register/+page.svelte -->
<script lang="ts">
import { enhance } from '$app/forms'
import type { SubmitFunction } from './$types'
import { Passlock, PasslockError } from '@passlock/sveltekit'
// we'll fill in the tenancyId and clientId later
const passlock = new Passlock({ tenancyId: 'TBC', clientId: 'TBC' })
// during form submission, ask Passlock to register a
// passkey on the user's device. This will return a
// secure token, representing the newly created passkey
const registerPasskey: SubmitFunction = async ({ cancel, formData }) => {
const email = formData.get('email') as string
const givenName = formData.get('givenName') as string
const familyName = formData.get('familyName') as string
const user = await passlock.registerPasskey({
email, givenName, familyName
})
if (!PasslockError.isError(user)) {
// attach the token to the request
formData.set('token', user.token)
} else {
cancel() // prevent form submission
alert(user.message)
}
}
</script>
<form method="post" use:enhance={registerPasskey}>
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 then use the Passlock library to register a passkey on the user's device. Passlock will store the public key component of the passkey in your Passlock vault.
If all goes well, this will return a token that we can exchange for a user object 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:
// src/routes/register/+page.server.ts
import type { Actions } from './$types'
import { PasslockError, TokenVerifier } from '@passlock/sveltekit'
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 user = await tokenVerifier.exchangeToken(token)
if (!PasslockError.isError(user)) {
console.log(user)
} else {
console.error(user.message)
}
}
}
The user includes a sub
(subject / user id) field. Later we'll use this to link the passkey registration to a local user.
Create a Passlock account
Within the +page.svelte
and +page.server.ts
files, 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 API Key
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 projects.
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 passlock = new Passlock({
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 will seem slow and clunky. We'll address the performance issues 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 for registration.
Create a route at src/routes/login/+page.svelte
:
<!-- src/routes/login/+page.svelte -->
<script lang="ts">
import { enhance } from '$app/forms'
import { Passlock, PasslockError } from '@passlock/sveltekit'
import type { SubmitFunction } from './$types'
import {
PUBLIC_PASSLOCK_TENANCY_ID,
PUBLIC_PASSLOCK_CLIENT_ID
} from '$env/static/public'
const passlock = new Passlock({
tenancyId: PUBLIC_PASSLOCK_TENANCY_ID,
clientId: PUBLIC_PASSLOCK_CLIENT_ID
})
const onSubmit: SubmitFunction = async ({ cancel, formData }) => {
const email = formData.get('email') as string
const user = await passlock.authenticatePasskey({ email })
if (!PasslockError.isError(user)) {
formData.set('token', user.token)
} else {
cancel() // prevent form submission
alert(user.message)
}
}
</script>
<form method="post" use:enhance={onSubmit}>
Email: <input type="text" name="email" /> <br />
<button type="submit">Login</button>
</form>
Notice how we're calling authenticatePasskey
and only passing the email this time.
// src/routes/login/+page.server.ts
import type { Actions } from './$types'
import { PasslockError, 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 user = await tokenVerifier.exchangeToken(token)
if (!PasslockError.isError(user)) {
console.log(user)
} else {
console.error(user.message)
}
}
}
The form action is the same (at this stage). Passlock abstracts passkey registration and authentication into a common structure.
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. The Passlock library ensures the challenge signature matches the public key then generates a secure token. We pass this token to the backend form action.
The form action ensures the token is valid, exchanging it for details about the user and the passkey used to authenticate.
Get the code
The final SvelteKit app is available in a GitHub repo Clone the repo and check out the tutorial/pt-1
tag.
Next steps
We've made a great start but we now need to link the passkeys to local user accounts and sessions. It's time to use Lucia authentication with passkeys.
Top comments (0)