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 will walk you through how to use the Clerk API to build custom flows for creating, signing users in with, and managing passkeys.
To use passkeys, first enable the strategy in the Clerk Dashboard.
Navigate to the Clerk Dashboard .
In the navigation sidebar, select User & Authentication > Email, Phone, and Username .
In the Authentication strategies section of this page, ensure Passkeys is enabled.
To create a passkey for a user, you must call User.createPasskey()
, as shown in the following example:
app /components /CustomCreatePasskeysButton.tsx 'use client'
import { useUser } from '@clerk/nextjs'
export function CreatePasskeyButton () {
const { user } = useUser ()
const createClerkPasskey = async () => {
try {
const response = await user ?.createPasskey ()
console .log (response)
} 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 now</ button >
}
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.
components /SignInWithPasskeyButton.tsx 'use client'
import { useSignIn } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'
export function SignInWithPasskeyButton () {
const { signIn } = useSignIn ()
const router = useRouter ()
const signInWithPasskey = async () => {
// 'discoverable' lets the user choose a passkey
// without autofilling any of the options
try {
const signInAttempt = await signIn ?.authenticateWithPasskey ({
flow : 'discoverable' ,
})
if ( signInAttempt ?.status === 'complete' ) {
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 >
}
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:
components /RenamePasskeyUI.tsx 'use client'
import { useUser } from '@clerk/nextjs'
import { PasskeyResource } from '@clerk/types'
import { useRef , useState } from 'react'
export function RenamePasskeyUI () {
const { user } = useUser ()
const passkeyToUpdateId = useRef < HTMLInputElement >( null )
const newPasskeyName = useRef < HTMLInputElement >( null )
const { passkeys } = user
const [ success , setSuccess ] = useState ( false )
const renamePasskey = async () => {
try {
const passkeyToUpdate = passkeys ?.find (
(pk : PasskeyResource ) => pk .id === passkeyToUpdateId . current ?.value ,
)
const response = await passkeyToUpdate ?.update ({
name : newPasskeyName . current ?.value ,
})
console .log (response)
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 = "ID of passkey to update" />
< input type = "text" placeholder = "New name" ref = {newPasskeyName} />
< button onClick = {renamePasskey}>Rename your passkey</ button >
< p >Passkey updated: {success ? 'Yes' : 'No' }</ p >
</>
)
}
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:
components /DeletePasskeyUI.tsx 'use client'
import { useUser } from '@clerk/nextjs'
import { useRef , useState } from 'react'
export function DeletePasskeyUI () {
const { user } = useUser ()
const passkeyToDeleteId = useRef < HTMLInputElement >( null )
const { passkeys } = user
const [ success , setSuccess ] = useState ( false )
const deletePasskey = async () => {
const passkeyToDelete = passkeys ?.find ((pk : any ) => pk .id === passkeyToDeleteId . current ?.value)
try {
const response = await passkeyToDelete ?.delete ()
console .log (response)
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 = "ID of passkey to delete" />
< button onClick = {deletePasskey}>Delete passkey</ button >
< p >Passkey deleted: {success ? 'Yes' : 'No' }</ p >
</>
)
}