Passkey login is gaining popularity faster than ever before, and for good reason. You must have noticed a new "Sign in with a passkey" option when logging into websites like GitHub or Vercel lately.
Passkeys are a new way to sign in to websites and apps without the need for passwords. They use your device's built-in unlock mechanism, such as Touch ID or Face ID, to authenticate your identity, providing a smooth, secure, and user-friendly login experience compared to traditional passwords and even two-factor authentication methods.
If you're looking to add a passkey login to your existing web app, you're in the right place! In this tutorial, we'll walk you through how you can add passkey authentication to your existing web app, regardless of the framework you're using.
We'll assume that you already have authentication set up, so we won't focus on that here. Instead, we'll dive straight into adding Passkeys to your existing auth system.
To bring passkeys to your app, you'll typically need two things:
So, let's get started on upgrading your app's login experience! To make it easier for you to follow along, we have created a sample app that you can use as a reference throughout the tutorial.
We'll be implementing passkey functionality in two phases.
This is what the general flow looks like. There will be two steps ("start" and "finalize"), which pass through the frontend, backend, and Passkey API.
1. Start Registration: Client requests server to start registration, server fetches creationOptions
from Passkey API, and passes them to the client.
2. Create Credential: Client creates the passkey using creationOptions
.
3. Finalize Registration: Client sends credential to server, server validates it with Passkey API, and confirms registration to client.
Let's implement it now in the code.
Make sure you have the passkey SDK installed in your backend.
The startServerPasskeyRegistration
function is an asynchronous function that takes a userID
parameter and does the following:
db.users
array using the userID
.passkeyApi.registration.initialize
with the user's ID and email (or an empty string if email is missing).createOptions
object from the SDK, which has the data needed to finish the registration on the client-side.The finishServerPasskeyRegistration
function is another asynchronous function that takes a credential
parameter. It simply calls passkeyApi.registration.finalize
with the credential
to complete the passkey registration on the server-side.
These functions are service-level functions that handle the logic for working with the Hanko Passkey SDK. They can be called from controller functions, which are then triggered by the corresponding routes in your application.
The handlePasskeyRegister()
controller function first extracts the user
object from the request (req
) and retrieves the userID
from it.
Next, it extracts the start
, finish
, and credential
properties from the request body. These properties are used to determine the specific action to be performed.
If the start
property is present, the controller function calls thestartServerPasskeyRegistration
service function, passing the userID
as an argument. This function initializes the passkey registration process and returns the createOptions
object. The controller then sends the createOptions
back to the client as a JSON response.
If the finish
property is present, the controller function calls the finishServerPasskeyRegistration
service function, passing the credential
obtained from the request body. This function finalizes the passkey registration process on the server-side. Once the registration is complete, the controller sends a JSON response with a success message.
Now, we setup a /passkeys/register
POST route. When a POST request is made to this, the handlePasskeyRegister
controller function is invoked to handle the passkey registration process.
Make sure to install the @github/webauthn-json
library in your frontend.
Next, create a registerPasskey
function that will be called when the user clicks a button to register a new passkey. It handles the client-side process of registering a passkey.
The function starts by sending a POST request to the server at the endpoint /api/passkeys/register
to initiate the passkey registration process. The request includes a JSON payload with the following properties:
start: true
: Indicates that the registration process is starting.finish: false
: Indicates that the registration process is not yet finished.credential: null
: Initially, no credential is provided.The function then awaits the response from the server and extracts the createOptions
from the response JSON.
Next, the function calls the create
function from @github/webauthn-json
library, passing the createOptions
as an argument.
After obtaining the credential
, the function sends another POST request to the same endpoint, but this time with a different payload:
start: false
: Indicates that the registration process is no longer starting.finish: true
: Indicates that the registration process is finished.credential
: The newly created passkey credential.Finally, if the response is successful, a passkey is registered for the user.
Now, to make it work we're missing one crucial step: getting the Tenant ID
and API key secret
from Hanko Cloud. For that, navigate over to Hanko, create an account, and set up your organization. Navigate to 'Create new project', select 'Passkey Infrastructure', and provide your project name and URL.
Note: It's recommended to create separate projects for your development and production environments. This way, you can use your frontend localhost URL for the development project and your production URL for the live project, ensuring a smooth transition between environments without the need to modify the 'App URL' later.
Obtain your Tenant ID
and create an API key, then add the Tenant ID
and API key secret
to your backend's '.env' file.
Now that you have your secrets added to your backend's .env
file, you should be all set to register a passkey. Go ahead and give it a try!
To enable passkey login, we'll create one more service function to initiate and finish the login process.
Now, similar to handlePasskeyRegister
create a handlePasskeyLogin
controller.
This function handles process of logging in with a passkey. It receives a request with start
, finish
, and options
properties. If start
is true, it initiates the login process by calling startServerPasskeyLogin()
and returns the login options. If finish
is true, it completes the login process by calling finishServerPasskeyLogin()
with the provided options
. It then retrieves the user ID from the JWT token, finds the corresponding user in the database, creates a session ID, sets the user for the session, and logs them in.
In the /passkeys/login
POST route, we invoke the above controller.
Now we create a signInWithPasskey
function. This function will interact with the backend APIs to initiate and finalize the passkey login process.
Congratulations! You now have a fully functional passkey login in your web app, significantly enhancing the user experience and making the authentication process more seamless for your users.
To further enhance the user's login experience, you can add autofill support to your email (or username) fields. This feature allows users to select their passkey directly from the autofill popup that appears when clicking on the input box, streamlining the login process.
When a user clicks on a passkey in the autofill popup, they will be immediately logged in, following the same flow as if they had clicked the "Sign in with passkey" button. This seamless integration provides a more intuitive and efficient way for users to access their accounts, eliminating the need for extra clicks or navigation.
To implement this feature, you'll need to make some additions to your login page. Let's start with that.
When the Login
component mounts, it checks if conditional mediation is available using the isConditionalMediationAvailable
function. If available, it automatically attempts to sign in with a passkey using the signInWithPasskeyAutofill
function.
The signInWithPasskey
function takes an optional autofill
parameter, which determines whether to use conditional mediation for the autofill popup. If autofill
is set to true
, the function sets the mediation
property of the loginOptions
to "conditional"
and assigns an AbortController
signal to cancel the autofill request if needed.
By adding the autoComplete="email webauthn"
attribute to the email input field, we enable the browser's autofill functionality to suggest available passkeys associated with the user's email address. When the user clicks on the input box, the autofill popup appears, allowing them to select their passkey and streamline the login process.
The "Sign in with a Passkey" button will always serve as a fallback option, allowing users to manually initiate the passkey sign-in flow if autofill is not available or if they prefer to use a different passkey.
In addition to implementing passkey autofill support, providing users with the ability to manage their passkeys can significantly enhance their experience. By offering features such as deleting, renaming, and listing available credentials, you give users greater control over their passkey credentials.
Create two more service-level functions listCredentials
and deleteCredential
.
The listCredentials
function takes a userID
as input and retrieves all the passkey credentials associated with that user, whereas deleteCredential
function will allow users to remove a specific passkey from their account. It takes a credentialID
as input, identifying the passkey to be deleted.
The listPasskeyCredentials
and deletePasskeyCredential
controller functions handle the API endpoints for retrieving and deleting passkey credentials. They utilize the listCredentials
and deleteCredential
service functions, respectively.
In the /passkeys/credentials
route, you can invoke the above controllers.
On the frontend, you can make API calls to fetch and delete passkey credentials. Here's an example of how you can achieve this:
That's it! You're done. So, along with adding a passkey authentication, we've also added autofill support and passkey management. Feel free to reach out to us on Discord if you get into issues.
Github repo: https://github.com/teamhanko/passkeys-webapp