Skip to main content

Register a passkey

The Passlock client library handles passkey creation and device registration. It also registers the passkey in your Passlock vault. You just need to link the Passlock user with a user entity in your own backend.

Prerequisites

You'll need to create a Passlock account (it's free). Once you've created your account, head to the settings section to obtain your:

  • Tenancy ID
  • Client ID
  • API Key

Install the client library

Add the Passlock client to your frontend:

npm install @passlock/client --save

Capture registration data

At a minimum you'll need the user's email, first and last names. For the purposes of this tutorial we'll just hard code some values. How you capture this information is up to you, Passlock is framework agnostic.

Why do we need the email?
  1. Passkeys are tied to a specific hostname. If you ever change your domain everything will break 💥 Similar to a reset password flow, storing the email allows you to authenticate a user and regenerate a passkey for the new domain.

  2. The browser needs to associate the passkey with a username. An email clearly identifies the specific account and is likely to be common across all authentication methods. If we don't use the email, a user could potentially use jdoe for your legacy username/password page, but johnd for their passkey account 🤮

Create a Passlock instance

You can instantiate a Passlock or PasslockUnsafe instance. The difference being that PasslockUnsafe rejects, whereas Passlock returns a union type: Promise<Result | PasslockError>. We'll use the "safe" Passlock class for this tutorial.

import { Passlock, PasslockError } from '@passlock/client'

const passlock = new Passlock({ tenancyId, clientId })

Register the passkey

The Passlock client library handles the creation and registration of the passkey. If the registration is successfull it returns a user and authStatement along with a token. You should forward this token to your backend as part of your registration flow.

register.ts
const result = await passlock.registerPasskey({ 
// capture this data from your user
email: 'jdoe@gmail.com',
givenName: 'John',
familyName: 'Doe',
// we'll explain this later :)
userVerification: 'discouraged'
})

// Passlock doesn't throw (by default), instead it returns
// PasslockError | Principal. Use PasslockError's static
// isError type guard to check for errors.
if (PasslockError.isError(result)) {
console.error(result)
} else {
// submit the token to your backend along with any other
// data captured on your registration form.
console.log("Token: %s", result.token)
}
User verification

You can ask the browser/device to perform local authentication before signing with the passkey. For now you'll disable this feature but we'll cover it later in the tutorial. You can read more about user verification.

Putting it all together

Here's a basic but complete flow. As Passlock is framework agnostic your production code will look different to this.

register.ts
import { Passlock, PasslockError } from '@passlock/client'

// you can find these details in the settings area of the Passlock console
const tenancyId = 'my-tenancy-id'
const clientId = 'my-client-id'

const passlock = new Passlock({ tenancyId, clientId })

const register = async () => {
// register the passkey
const result = await passlock.registerPasskey({
email: 'jdoe@gmail.com',
givenName: 'John',
familyName: 'Doe',
userVerification: 'discouraged'
})

if (PasslockError.isError(result)) {
console.error(result)
} else {
// submit to your backend
console.log('Token: %s', result.token)
}
}

Backend processing

Our motivation for delivering Passlock as a serverless platform vs a server side library is the ability to support pretty much any backend. Your backend just needs to call a REST endpoint to exchange the token for a user and authStatement.

Your backend should make a GET request to https://api.passlock.dev/{tenancyId}/token/{token}, replacing tenancyId and token with the appropriate values. You will also need to provide your API Key as an authorization header:

GET https://api.passlock.dev/{tenancyId}/token/{token}
Host: https://api.passlock.dev
Accept: application/json
Authorization: Bearer <API Key>

For now you'll just use curl to exchange the generated token for the user and auth statement. Replace the $API_KEY, $TENANCY and $TOKEN placeholders.

curl -s -H "Authorization: Bearer $API_KEY" "https://api.passlock.dev/$TENANCY/token/$TOKEN" | jq
output
{
"token": "2arafoq-8coasjl-qx4jz3x",
"user": {
"id": "khXCYCxcGwJTLoaG6kVxB",
"email": "jdoe@gmail.com",
"givenName": "John",
"familyName": "Doe",
"emailVerified": false
},
"authStatement": {
"authType": "passkey",
"userVerified": false,
"authTimestamp": "2024-01-25T12:01:07.295Z"
},
"expiresAt": "2024-01-25T12:06:07.000Z"
}
tip

jq is a really neat json formatter for the unix/mac command line.

Assuming you receive a 200 response, you can create (or update) a user in your database, linking their Passlock user.id to their account. During subsequent authentication, you will receive the same Passlock user id, which you'll use to lookup your user's record.

warning

Don't use the Passlock user id as your database primary key. Generate your own PK, but store (and index) the Passlock user id

Next steps

If you tested the code yourself you've hopefully registered a passkey on your own device, with the username jdoe@gmail.com (or whatever email you hard coded). If you check the Passlock console you should see the new user. Now it's time to learn how to authenticate a user using their passkey...