Skip to content

Add passkeys

Let your end-users sign in with nothing typed, and manage the credentials themselves.

A passkey is a credential that lives on the user’s device and never leaves it. Once someone has enrolled one, they sign in without typing anything — no password, and on most devices no email address either, because the authenticator is what names them. When the device verified the person behind it — a fingerprint, a face, a device PIN — that single gesture is two factors, and the session it mints says so.

Passkeys are off until you turn them on, per product, and they work in both roles a WebAuthn credential can play: a first factor that replaces the password, and a second factor over any other first factor.

What your users get

A sign-in with nothing to type. The browser offers the passkeys it holds in its own autofill dropdown over your email field, the user picks one, and the flow finishes. There is no identifier to enter and nothing to remember.

Nothing to phish. The credential is bound to your domain by the browser, so a look-alike site cannot ask for it and the user cannot be talked into handing it over. The private half never leaves the authenticator; what we store is the public half, which signs nothing.

One gesture, two factors. A device that verified the user before asserting gives them an aal2 session immediately — see the assurance table.

One passkey per device, named. People enrol a passkey on their laptop and another on their phone, and each is listed under a name they chose, with how it is reached and when it was last used.

Turn them on

The Passkeys section of Users → Configuration is the whole switch, and Offer passkeys is the only control most products touch. The same block is written through the product’s API, where the field is enabled:

{ "passkeys": { "enabled": true } }

Your allowed origins then have to decide one thing for the browser: the domain the credentials belong to. If every allowed origin is on one host, that host is used and there is nothing else to set — leave Relying party ID empty. If your product runs on two hosts — app.example.com and admin.example.com — you name their common parent explicitly, as the bare domain with no scheme or port:

{ "passkeys": { "enabled": true, "rpId": "example.com", "rpName": "Acme" } }

Configure authentication has the block field by field, the exact rule the domain is resolved by, and the reason it is refused rather than guessed.

Sign in with one

<SignIn/> from @lessly/users-react 0.10.0 or later needs nothing from you. It renders a passkey button whenever the browser can run a ceremony, and while the sign-in form is open it also puts a passkey offer in the browser’s autofill dropdown over the email field. Pass passkeys={false} to turn both off for a product that has not enabled them.

Writing your own form, the sign-in is one call:

import { usePasskeySignIn } from '@lessly/users-react'

function PasskeyButton() {
  const passkey = usePasskeySignIn()
  if (!passkey.supported) return null

  return (
    <button type="button" disabled={passkey.busy} onClick={() => passkey.signIn()}>
      Sign in with a passkey
    </button>
  )
}

Use the client libraries has the autofill offer, the headless @lessly/users-client calls underneath, and the feature detection that decides whether to draw the button at all.

What a passkey sign-in is worth

What the authenticator didThe session
Verified the user — fingerprint, face, PINaal2, and the second-factor challenge is skipped
Presence only, no verificationaal1, and the second-factor challenge runs as usual

A verified assertion is possession of the device and something only that person can present, in one gesture. Asking such a sign-in for a second factor would be asking for a third, so Lessly Users does not: the flow completes, and the token carries aal: 'aal2' and webauthn among its amr methods. A product with mfa.required set has its floor met by that sign-in alone.

An unverified assertion is one factor and is treated as one. The account’s other factors are offered as usual — minus passkeys themselves, because a factor cannot be its own second factor.

Use a passkey as the second factor

A user who holds a passkey holds a verified factor, so it appears wherever factors appear: in their factor list, and as an answer to the second-factor challenge after a password, an email code, a magic link or an OAuth sign-in. <SignIn/> offers it on that screen when the parked attempt names it; on your own form the parked attempt lists webauthn among its factors and submitSecondFactorPasskey() is the way through.

Add two-factor authentication covers the second-factor screen as a whole.

Let users manage them

<PasskeySettings/> is the self-service plane as one import — the list, Add a passkey, renaming a row, and removing one:

import { PasskeySettings, SignedIn } from '@lessly/users-react'

export function SecuritySettings() {
  return (
    <SignedIn>
      <PasskeySettings />
    </SignedIn>
  )
}

If you already render <UserProfile/>, you are done. Its Security section embeds this same surface beside two-step verification, so a product with the account panel in it has passkey self-service already.

Adding, renaming and removing are sensitive: Lessly Users asks the user to prove it is still them before each one, and the component renders that prompt itself. The prompt takes a passkey as readily as a code — which is what keeps a user whose only credential is a passkey from being locked out of the screen that manages it.

Handle a lost device

The user removes that passkey from their own settings, from any other device they are signed in on. It stops working immediately, and the credential still on the lost device asserts nothing.

If they cannot get in at all, someone on your side revokes that one passkey from the user’s record — Revoke on the passkey’s row of the MFA factors section, and the same call is available through the product’s API and to an agent. It takes a written reason, is recorded verbatim in the audit trail, takes effect immediately rather than after a delay — a lost device is exactly the case where waiting is the wrong answer — and is refused when that passkey is the account’s last way in. Manage your end-users has it alongside the factor reset.

Nobody at Lessly can lift a passkey off one of your users, and no operator anywhere can add one.

Browser support

Passkeys need a browser with WebAuthn and a platform or security-key authenticator; the autofill offer needs conditional mediation on top of that, which is newer. Both are detected rather than assumed — supported gates the button and autofillAvailable gates the offer — so a browser that has neither simply sees your other sign-in methods. There is nothing to configure for it and no user agent to sniff.

Next steps

Was this page helpful?
Esc

Start typing to search the docs.

navigateselect