Skip to content

Passkey registration

Passkey creation/registration is a three-step process:

  1. Authorised in your backend
  2. Created in your frontend
  3. Verified and linked to an account in your backend
sequenceDiagram
  participant Frontend
  participant Backend
  participant Browser as @passlock/browser
  participant Server as @passlock/server

  Backend->>Server: authorizePasskeyRegistration()
  Server-->>Backend: registrationToken
  Backend-->>Frontend: registrationToken

  Frontend->>Browser: registerPasskey({ registrationToken })
  Browser-->>Frontend: code

  Frontend->>Backend: code
  Backend->>Server: exchangeCode({ code })
  Server-->>Backend: authenticatorId
  Backend->>Backend: assignPasskey({ userId, authenticatorId })

Note: this could be in response to a request from your frontend. i.e. a user clicking a “create passkey” button, resulting in a fetch call to an API endpoint:

backend/register.ts
import { Passlock } from "@passlock/server";
// found in your tenancy settings section of the Passlock console
const passlock = new Passlock({
tenancyId: "MyTenancyId",
apiKey: "MyApiKey"
});
const user = await getCurrentUser();
const result = await passlock.authorizePasskeyRegistration({
rpId: "example.com", // or localhost
rpName: "Example App",
userId: user.id,
username: user.email,
displayName: user.name, // optional
excludeCredentials: user.passkeyIds, // optional
userVerification: "preferred", // optional
timeout: 5000 // optional
});
if (result.success) {
// provide this to your frontend code
// see frontend/register.ts below
return result.authenticationToken;
}
rpIdRelying Party ID
userIdYour internal user ID
usernameUsed by the device to identify the account associated with the passkey. Typically a username, user ID or email address.
displayNamemay be displayed by the user’s passkey manager, although the username is typically used instead. Prompt the user to choose a meaningful name e.g “Personal account”, “Work account”, etc.
excludeCredentialsTo prevent the user registering more than one passkey for a given account, pass the user’s existing passkey IDs here.
userVerificationSet to preferred, required, or discouraged to control the user verification behavior
timeoutPeriod the user has to complete the authentication process in milliseconds. Important: don’t set this when using autofill

If you cannot use the passlock server library, use the v2 REST endpoint to obtain a registration token:

POST /v2/{tenancyId}/passkey/registration/authorize HTTP/1.1
Host: api.passlock.dev
Accept: application/json
Content-Type: application/json
Authorization: Bearer {apiKey}
{
"rpId": "example.com",
"userId": "local-user-id",
"username": "jdoe@example.com",
"displayName": "Jane Doe",
"userVerification": "preferred"
}

Your frontend code will obtain a passkeyRegistrationToken from your backend, and use it to trigger passkey registration on the device:

frontend/register.ts
import { Passlock } from "@passlock/browser";
const passlock = new Passlock({ tenancyId: "MyTenancyId" });
createPasskeyButton.addEventListener("click", async () => {
// see backend/register.ts above
const registrationToken = await fetchTokenFromBackend();
const result = await passlock.registerPasskey({
registrationToken
});
if (result.success) {
// see backend/register.ts below
await submitCodeToBackend(result.value.code);
// alternatively use the id_token (JWT)
await submitIdTokenToBackend(result.value.id_token);
}
});

Exchange the code for details about the passkey:

backend/register.ts
import { Passlock } from "@passlock/server";
const passlock = new Passlock({
tenancyId: "MyTenancyId",
apiKey: "MyApiKey"
});
const result = await passlock.exchangeCode({ code });
// alternatively verify the id_token (JWT)
const result = await passlock.verifyIdToken({ id_token });
if (result.success) {
// link the passkey to a local user account in your database
await assignPasskeyToUser(userId, result.value.authenticatorId);
};

If you cannot use the passlock server library, use the v2 REST endpoint:

GET /v2/{tenancyId}/principal/{code} HTTP/1.1
Host: api.passlock.dev
Accept: application/json
Authorization: Bearer {apiKey}

The response is an ExtendedPrincipal. As with the server-library flow, verify that the returned userId matches the local user who started registration, then store the returned authenticatorId as the Passlock passkey ID for that user.

See error handling to understand how to handle Passlock errors.

If you didn’t first check whether the user’s device supports passkeys you could run into a PasskeyUnsupportedError error, tagged as @error/PasskeyUnsupported:

import { Passlock, isPasskeyUnsupportedError } from "@passlock/browser";
const result = await passlock.registerPasskey({ ... });
if (!result.success) {
if (isPasskeyUnsupportedError(result.error)) {
alert("Passkeys not supported on this device");
}
}

If you supply excludeCredentials during the preparation step you could run into a DuplicatePasskeyError error, tagged as @error/DuplicatePasskey:

import { Passlock, isDuplicatePasskeyError } from "@passlock/browser";
const result = await passlock.registerPasskey({ ... });
if (result.failure) {
if (isDuplicatePasskeyError(result.error)) {
alert("You already have a suitable passkey on this device");
}
}

You could also encouter another (unexpected) error, OtherPasskeyError tagged as @error/OtherPasskey. This error includes a code, indicating the specific error along with a message.

backend/register.ts
const result = await passlock.authorizePasskeyRegistration({
rpId: "example.com", // .com
rpName: "Example App",
});

Code running on example.co.uk:

frontend/register.ts
import { Passlock, isOtherPasskeyError } from "@passlock/browser";
const result = await passlock.registerPasskey({ ... });
if (!result.success) {
if (isOtherPasskeyError(result.error)) {
if (result.error.code === "ERROR_INVALID_RP_ID") {
alert("Expected example.com but running on example.co.uk")
}
}
}

We’ve made passkey registration flexible. Feel free to implement it in a way that suits your needs and technical stack. These are just some recommendations:

Always test for device support before attempting to register a passkey on the device. Alternatively, make sure you handle the PasskeyNotSupportedError error.

Use the excludeCredentials property to prevent the user registering duplicate passkeys for the same account. Be sure to handle the potential DuplicatePasskeyError

Attestation is an advanced concept which essentially allows you to obtain information about the authenticator (device) generating the passkey. If you want to restrict users to a list of approved platforms, attestation can be used for this.