Build a custom authentication flow using passkeys
Clerk supports passwordless authentication via passkeys, enabling users to sign in without having to remember a password. Instead, users select a passkey associated with their device, which they can use to authenticate themselves.
This guide demonstrates how to use the Clerk API to build a custom user interface for creating, signing users in with, and managing passkeys.
Enable passkeys
To use passkeys, first enable the strategy in the Clerk Dashboard.
- In the Clerk Dashboard, navigate to the Email, phone, username page.
- In the Authentication strategies section of this page, ensure Passkeys is enabled.
Domain restrictions for passkeys in development
Passkeys are tied to the domain they are created on and cannot be used across different domains. For example:
- Passkeys created on
localhost
will only work onlocalhost
. - Passkeys created on your Account Portal (e.g.,
your-app.accounts.dev
) will only work on that domain.
To work around this in development, you can either:
- Use Clerk's Components, Elements, or custom flows, instead of using Account Portal.
- Create the passkey directly through Account Portal instead of your local application.
This issue does not affect production environments, as your Account Portal will be hosted on a subdomain of your main domain (e.g., accounts.your-domain.com
), enabling passkeys to work seamlessly across your application.
Create user passkeys
To create a passkey for a user, you must call User.createPasskey()
, as shown in the following example:
export function CreatePasskeyButton() {
const { user } = useUser()
const createClerkPasskey = async () => {
if (!user) return
try {
await user?.createPasskey()
} catch (err) {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
console.error('Error:', JSON.stringify(err, null, 2))
}
}
return <button onClick={createClerkPasskey}>Create a passkey</button>
}
Sign a user in with a passkey
To sign a user into your Clerk app with a passkey, you must call SignIn.authenticateWithPasskey()
. This method allows users to choose from their discoverable passkeys, such as hardware keys or passkeys in password managers.
export function SignInWithPasskeyButton() {
const { signIn } = useSignIn()
const router = useRouter()
const signInWithPasskey = async () => {
// 'discoverable' lets the user choose a passkey
// without auto-filling any of the options
try {
const signInAttempt = await signIn?.authenticateWithPasskey({
flow: 'discoverable',
})
if (signInAttempt?.status === 'complete') {
await setActive({ session: signInAttempt.createdSessionId })
router.push('/')
} else {
// If the status is not complete, check why. User may need to
// complete further steps.
console.error(JSON.stringify(signInAttempt, null, 2))
}
} catch (err) {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
console.error('Error:', JSON.stringify(err, null, 2))
}
}
return <button onClick={signInWithPasskey}>Sign in with a passkey</button>
}
Rename user passkeys
Clerk generates a name based on the device associated with the passkey when it's created. Sometimes users may want to rename a passkey to make it easier to identify.
To rename a user's passkey in your Clerk app, you must call the update()
method of the passkey object, as shown in the following example:
export function RenamePasskeyUI() {
const { user } = useUser()
const { passkeys } = user
const passkeyToUpdateId = useRef<HTMLInputElement>(null)
const newPasskeyName = useRef<HTMLInputElement>(null)
const [success, setSuccess] = useState(false)
const renamePasskey = async () => {
try {
const passkeyToUpdate = passkeys?.find(
(pk: PasskeyResource) => pk.id === passkeyToUpdateId.current?.value,
)
await passkeyToUpdate?.update({
name: newPasskeyName.current?.value,
})
setSuccess(true)
} catch (err) {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
console.error('Error:', JSON.stringify(err, null, 2))
setSuccess(false)
}
}
return (
<>
<p>Passkeys:</p>
<ul>
{passkeys?.map((pk: PasskeyResource) => {
return (
<li key={pk.id}>
Name: {pk.name} | ID: {pk.id}
</li>
)
})}
</ul>
<input ref={passkeyToUpdateId} type="text" placeholder="Enter the passkey ID" />
<input type="text" placeholder="Enter the passkey's new name" ref={newPasskeyName} />
<button onClick={renamePasskey}>Rename passkey</button>
<p>Passkey updated: {success ? 'Yes' : 'No'}</p>
</>
)
}
Delete user passkeys
To delete a user's passkey from your Clerk app, you must call the delete()
method of the passkey object, as shown in the following example:
export function DeletePasskeyUI() {
const { user } = useUser()
const { passkeys } = user
const passkeyToDeleteId = useRef<HTMLInputElement>(null)
const [success, setSuccess] = useState(false)
const deletePasskey = async () => {
const passkeyToDelete = passkeys?.find((pk: any) => pk.id === passkeyToDeleteId.current?.value)
try {
await passkeyToDelete?.delete()
setSuccess(true)
} catch (err) {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
console.error('Error:', JSON.stringify(err, null, 2))
setSuccess(false)
}
}
return (
<>
<p>Passkeys:</p>
<ul>
{passkeys?.map((pk: any) => {
return (
<li key={pk.id}>
Name: {pk.name} | ID: {pk.id}
</li>
)
})}
</ul>
<input ref={passkeyToDeleteId} type="text" placeholder="Enter the passkey ID" />
<button onClick={deletePasskey}>Delete passkey</button>
<p>Passkey deleted: {success ? 'Yes' : 'No'}</p>
</>
)
}
Feedback
Last updated on