Skip to main content

Frontend client

Our frontend client library is framework agnostic, largely because the user interface is controlled by the device so there's nothing to style and no validation to perform 🎉

Installing the library​

npm install @passlock/client --save
Why no UMD build?

We're assuming that your web app is bundled using something like webpack, rollup or vite. We emit tree shakable ES2015 modules so you're only pulling in what you need.

We believe bundling is the reposonsibility of the application, not the library

Error handling​

All errors are wrapped into a PasslockError which extends Error to include a Passlock specific error code:

PasslockError​

export class PasslockError extends Error {
readonly message: string
readonly code: ErrorCode
}

The error code acts as a descriminator:

export enum ErrorCode {
NotSupported: 'NotSupported',
BadRequest: 'BadRequest',
Duplicate: 'Duplicate',
Forbidden: 'Forbidden',
InternalBrowserError: 'InternalBrowserError',
InternalServerError: 'InternalServerError',
NetworkError: 'NetworkError',
NotFound: 'NotFound',
Disabled: 'Disabled',
Unauthorized: 'Unauthorized'
}

We don't throw/reject​

The Passlock class doesn't throw/reject. In cases where an operation could fail, we return a union type:

export type AuthenticateFn: (options: AuthOptions) => Promise<PasslockError | Principal>

PasslockError type guard​

You can test for PasslockErrors using PasslockError's static isError type guard:

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

const passlock = new Passlock({ ... })

const result = await passlock.authenticatePasskey()

if (PasslockError.isError(result) && result.code === ErrorCode.Disabled) {
console.error("User is disabled")
}

Opting to reject​

If you prefer to handle errors through the typical promise rejection pattern, use the PasslockUnsafe class:

import { PasslockUnsafe, PasslockError, ErrorCode } from '@passlock/client'

const passlock = new PasslockUnsafe({ ... })

try {
await passlock.authenticatePasskey(...)
} catch (e) {
if (PasslockError.isError(e) && e.code === ErrorCode.Disabled) {
...
}
}

Common Types​

Principal​

In addition to PasslockError the other common type is Principal:

export type Principal = {
token: string // pass this to your backend
user: {
id: string // Passlock's user id
givenName: string
familyName: string
email: string
emailVerified: boolean
}
authStatement: {
authType: 'email' | 'passkey' | 'google' // how the user authenticated
userVerified: boolean // did the device re-authenticate the user?
authTimestamp: Date
}
expiresAt: Date
}
example
{
"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"
}

Principal represents a successful registration or authentication operation. The token should be passed to your backend and exchanged for the same Principal via the REST API.

The Passlock class​

Create an instance of either the Passlock or PasslockUnsafe class. Note you'll need your tenancyId and cliendId (which can be found in your Passlock console under settings):

import { Passlock } from '@passlock/client'

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

Aborting operations​

Most functions accept an optional signal parameter of type AbortSignal, allowing you to abort the operation early:

const controller = new AbortController()
const signal = controller.signal

const isRegisteredPromise = await passlock.isExistingUser(
{ tenancyId, clientId, email }, { signal }
)

controller.abort() // cancels the operation

Check for passkey support​

// Local check, no calls to the Passlock backend
const isPasskeySupport = () => Promise<boolean>

In browser terms, there is no such thing as a 'passkey'. Passkey is just a term used to describe a set of browser capabilities within the broader Web Authentication API. Behind the scenes we test for the capabilities, with some workarounds to deal with browser/device quirks.

Check for conditional UI support​

// Local check, no calls the the Passlock backend
const isAutofillSupport = () => Promise<boolean>

Note: this function calls isPasskeySupport.

Check for an existing user​

export type Request = {
tenancyId: string
clientId: string
email: string,
}

export type Options = {
signal?: AbortSignal
}

const isExistingUser = (request: Request, options: Options) => Promise<PasslockError | boolean>

Register a passkey​

This function will create and register a passkey in the browser and store the public key in Passlock's backend vault.

export type UserVerification = "required" | "preferred" | "discouraged"

export type VerifyEmailLink = {
method: 'link'
// your url - must comply with the RP ID
redirectUrl: string
}

export type VerifyEmailCode = {
method: 'code'
}

export type VerifyEmail = VerifyEmailLink | VerifyEmailCode

export type Request = {
email: string
givenName: string
familyName: string
// defaults to preferred if not specified
userVerification?: UserVerification
// redirectUrl must be set if 'link'
verifyEmail?: VerifyEmail
}

const registerPasskey = (request: Request, options: Options) => Promise<PasslockError | Principal>

Authenticate a passkey​

export type UserVerification = "required" | "preferred" | "discouraged"

export type Request = {
tenancyId: string
clientId: string
email?: string
userVerification?: UserVerification
}

const authenticatePasskey = (request: Request, options: Options) => Promise<PasslockError | Principal>

By default this will allow the user to authenticate with any valid passkey (for the RP ID). If you pass an email to the authenticate function, the browser will select the matching Passkey for them.

info

Why might you want to do this? It's something of an edge case, but if the user is already logged in to your app and you want to reauthenticate them, for example to perform an action on their account, you might want to restrict the passkey to the one matching the current account. Of course this is only beneficial if users are likely to have multiple accounts in your system, otherwise the browser will select the sole passkey anyway.

Verify an email address​

See the verify email howto

export type Request = {
code: string
}

// will take the ?code=123 from the current window.location
const verifyEmailLink = () => Promise<PasslockError | Principal>

// prompt the user for the code and pass it
const verifyEmailCode = (request: Request, options: Options) => Promise<PasslockError | Principal>