Passkey registration
Passkey creation/registration is a three-step process:
- Authorised in your backend
- Created in your frontend
- 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 })
Backend: authorise registration
Section titled “Backend: authorise registration”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:
import { Passlock } from "@passlock/server";
// found in your tenancy settings section of the Passlock consoleconst 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;}Registration initiation options
Section titled “Registration initiation options”| rpId | Relying Party ID |
|---|---|
| userId | Your internal user ID |
| username | Used by the device to identify the account associated with the passkey. Typically a username, user ID or email address. |
| displayName | may 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. |
| excludeCredentials | To prevent the user registering more than one passkey for a given account, pass the user’s existing passkey IDs here. |
| userVerification | Set to preferred, required, or discouraged to control the user verification behavior |
| timeout | Period the user has to complete the authentication process in milliseconds. Important: don’t set this when using autofill |
Alternative: Using the REST API
Section titled “Alternative: Using the REST API”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.1Host: api.passlock.devAccept: application/jsonContent-Type: application/jsonAuthorization: Bearer {apiKey}
{ "rpId": "example.com", "userId": "local-user-id", "username": "jdoe@example.com", "displayName": "Jane Doe", "userVerification": "preferred"}Frontend: create the passkey
Section titled “Frontend: create the passkey”Your frontend code will obtain a passkeyRegistrationToken from your backend, and use it to trigger passkey registration on the device:
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); }});Backend: verify the passkey
Section titled “Backend: verify the passkey”Exchange the code for details about the passkey:
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);};Alternative: Using the REST API
Section titled “Alternative: Using the REST API”If you cannot use the passlock server library, use the v2 REST endpoint:
GET /v2/{tenancyId}/principal/{code} HTTP/1.1Host: api.passlock.devAccept: application/jsonAuthorization: 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.
Error handling
Section titled “Error handling”See error handling to understand how to handle Passlock errors.
No passkey support
Section titled “No passkey support”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"); }}Duplicate passkey
Section titled “Duplicate passkey”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"); }}Other passkey error
Section titled “Other passkey error”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.
const result = await passlock.authorizePasskeyRegistration({ rpId: "example.com", // .com rpName: "Example App",});Code running on example.co.uk:
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") } }}Tips and recommendations
Section titled “Tips and recommendations”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:
Test for device support
Section titled “Test for device support”Always test for device support before attempting to register a passkey on the device. Alternatively, make sure you handle the PasskeyNotSupportedError error.
Preventing duplicate passkeys
Section titled “Preventing duplicate passkeys”Use the excludeCredentials property to prevent the user registering duplicate passkeys for the same account. Be sure to handle the potential DuplicatePasskeyError
Attestation
Section titled “Attestation”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.