Passkey authentication
Similar to passkey registration, authentication is a three-step process:
- Authorised in your backend
- Authenticated in your frontend
- Verified in your backend
sequenceDiagram
participant Frontend
participant Backend
participant Browser as @passlock/browser
participant Server as @passlock/server
Frontend->>Backend: authorize authentication
Backend->>Server: authorizePasskeyAuthentication({ rpId, discoverable: true })
Server-->>Backend: authenticationToken
Backend-->>Frontend: authenticationToken
Frontend->>Browser: authenticatePasskey({ authenticationToken })
Browser-->>Frontend: id_token, code
Frontend->>Backend: code
Backend->>Server: exchangeCode(code)
Server-->>Backend: userId
Backend->>Backend: lookupUser(userId)
Backend: authorise authentication
Section titled “Backend: authorise authentication”Note: this could be in response to a request from your frontend. i.e. a user clicking a “sign in” button, resulting in a fetch call to an API endpoint:
import { Passlock } from "@passlock/server";
const passlock = new Passlock({ tenancyId: "MyTenancyId", apiKey: "MyApiKey"});
// userId and allowCredentials are optional// and only make sense if the userId is known (see options)const result = await passlock.authorizePasskeyAuthentication({ rpId: "example.com", // or localhost userId: user.id, allowCredentials: user.passkeys, discoverable: true, userVerification: "preferred", timeout: 5000, mediation: "conditional"});
if (result.success) { // provide this to your frontend code // see frontend/authenticate-passkey.ts below return result.authenticationToken;}Authentication initiation options
Section titled “Authentication initiation options”| rpId | The Relying Party ID |
|---|---|
| allowCredentials | Optional list of passkey IDs to pre-select for the known account. The device will limit authentication to one of those passkeys. |
| userId | Shortcut for allowCredentials. Passlock will look up the user’s known credentials (passkeys) |
| discoverable | If you don’t know the user ID, set this to true. The device will present all passkeys for the given Relying Party ID |
| 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 |
| mediation | Set to true to enable 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 an authentication token:
POST /v2/{tenancyId}/passkey/authentication/authorize HTTP/1.1Host: api.passlock.devAccept: application/jsonContent-Type: application/jsonAuthorization: Bearer {apiKey}
{ "rpId": "example.com", "discoverable": true, "userVerification": "preferred"}Frontend: authenticate the passkey
Section titled “Frontend: authenticate the passkey”Your frontend code will obtain a passkeyAuthenticationToken from your backend, and use it to trgger passkey authentication on the device:
import { Passlock } from "@passlock/browser";
const passlock = new Passlock({ tenancyId: "MyTenancyId" });
loginButton.addEventListener("click", async () => { // see backend/initiate-authentication.ts above const authenticationToken = await fetchTokenFromBackend();
const result = await passlock.authenticatePasskey({ authenticationToken });
if (result.success) { // see backend/verify-authentication.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 returned from the frontend for a verified authentication result:
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) { const user = await lookupUserById(result.value.userId); // alternatively identify the user by their passkey id const user = await lookupUserByPasskeyId(principal.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.authenticatePasskey({ ... });
if (!result.success) { if (isPasskeyUnsupportedError(result.error)) { alert("Passkeys not supported on this device"); }}Orphaned passkey
Section titled “Orphaned passkey”authenticatePasskey can return an OrphanedPasskeyError error, tagged as @error/OrphanedPasskey:
import { Passlock, isOrphanedPasskeyError,} from "@passlock/browser";
const result = await passlock.authenticatePasskey({ ... });
if (result.failure) { if (isOrphanedPasskeyError(result.error)) { alert("This passkey is no longer valid"); }}This error occurs when a user uses a passkey on their device that no longer exists in the vault.
Tell the user the passkey is stale and give them a way to remove it from their password manager. For passkeys that still have a known vault record, use the prepared deletePasskeys flow described in handling an OrphanedPasskeyError.
Other passkey error
Section titled “Other passkey error”You could also encounter 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.authorizePasskeyAuthentication({ rpId: "example.com", // .com discoverable: true});Code running on example.co.uk:
import { Passlock, isOtherPasskeyError } from "@passlock/browser";
const result = await passlock.authenticatePasskey({ ... });
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 authentication 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”Don’t assume that because the user registered a passkey they are able to authenticate with it. They might be using a different device which does not support passkeys. Always test for device support before attempting to authenticate.
Two-step login
Section titled “Two-step login”Unless you’re starting out with a green field project, you’ll likely have existing users. Some of these users will not yet have registered a passkey. In this case, you can implement a two-step login flow.
The user first enters their account identifier (username/email). You check if that account has a passkey registered, and if so prompt them to authenticate with it in the second step.
Browser autofill
Section titled “Browser autofill”Alternatively browser autofill aka conditional mediation allows you to present a “legacy” username/password form, however the browser will prompt the user to use their passkey if they have one on their device.
Step up authentication
Section titled “Step up authentication”Passkeys are often used to re-authenticate logged in users. You might choose to re-authenticate a user for a security sensitive operation such as changing their email address or PIN. In this scenario setting user verification to required usually makes sense.
Handling missing passkeys
Section titled “Handling missing passkeys”Passkeys are comprised of two components - a private key, stored on the user’s device, and a public key, stored in your Passlock vault. If either component is missing the user will receive an error. Learn how to handle missing passkeys gracefully.
Domain migration
Section titled “Domain migration”Passkeys are resistant to phishing attacks because they are bound to a specific domain (Relying Party ID). You can’t simply instruct users to sign in with their old-domain.com passkey on new-domain.com. Learn how Passlock’s domain migration tooling makes this possible.