Passkey authentication is gaining popularity faster than ever before. You must have probably noticed a new 'Sign in with a Passkey' option when logging into places like Github or the Apple Store by now.
With passkeys, your users can sign in without needing any passwords at all. Instead, it uses Touch ID or Face ID on their device to authenticate them, which makes it way more secure and easier to use than old-school passwords or even the two-factor authentication methods we're used to. On top of that, it provides a super smooth login experience too.
In this tutorial, we'll show you how you can add passkey login for your users to your React application using Hanko Passkey API. While we're using Express.js for our backend, the process will be pretty similar if you're working with any other JavaScript or TypeScript-based backend.
We've already built a basic auth and in this guide, we'll focus specifically on how you can add a passkey login to your existing auth system. So, no reinventing the wheel here; instead, we'll show you how to integrate passkeys seamlessly with your existing authentication flow. Check out our demo app repo here.
Let's get to building.
Install the Passkey SDK
Install the JavaScript SDK provided by Hanko and webauth-json package by GitHub. Note that you need to install the SDK on the backend directory and webauthn-json package on the frontend directory.
When it comes to passkey registration, you'll need to decide where and how your users can add passkeys to their accounts. It's entirely up to you. In our case, we've decided to provide a 'Register Passkey' button on the 'dashboard' page.
On your express backend, you’ll have to use tenant({ ... }).registration.initialize() and .registration.finalize() to create and store a passkey for your user. We'll create a /api/passkeys/register endpoint that will utilize these.
On your frontend, you’ll have to call create() from @github/webauthn-json with the object .registration.initialize() returned.
First, create two services startServerPasskeyRegistration() and finishServerPasskeyRegistration() in the /express-backend/services/passkey.js
In the startServerPasskeyRegistration() function we retrieve the user's information from a database and initiate the passkey registration process by calling the passkeyApi.registration.initialize() method with the user's ID and email. This function returns the options required for creating a new passkey on the client side. The finishServerPasskeyRegistration() function finalizes the registration process by sending the client-side credential to the passkey API.
These functions are then exported to be used in the express-backend/controllers/passkey.js .
import { tenant } from "@teamhanko/passkeys-sdk";
import dotenv from "dotenv";
import db from "../db.js";
dotenv.config();
const passkeyApi = tenant({
apiKey: process.env.PASSKEYS_API_KEY,
tenantId: process.env.PASSKEYS_TENANT_ID,
});
async function startServerPasskeyRegistration(userID) {
const user = db.users.find((user) => user.id === userID);
const createOptions = await passkeyApi.registration.initialize({
userId: user.id,
username: user.email || "",
});
return createOptions;
}
async function finishServerPasskeyRegistration(credential) {
await passkeyApi.registration.finalize(credential);
}
export {
startServerPasskeyRegistration,
finishServerPasskeyRegistration,
};
Now we definehandlePasskeyRegister() controller that handles two stages of the passkey registration process: initiation and completion. The function utilizes the helper functions startServerPasskeyRegistration() and finishServerPasskeyRegistration() we created above.
Now, you can use it in the specific route, here we do it in the /api/passkeys/register endpoint.
import express from "express";
const router = express.Router();
import { handlePasskeyRegister, handlePasskeyLogin } from "../controllers/passkey.js";
import { checkAuth } from "../middleware/auth.js";
router.post("/passkeys/register", checkAuth, handlePasskeyRegister);
export default router;
The backend part is done, now it's time to add the 'Register Passkey' button on the frontend.
Here, the registerPasskey() function handles the passkey registration process. It first sends a request to the server to initiate the registration process and receives the response for creating a new passkey. It then uses the @github/webauthn-json library to create a new passkey credential based on the received options from the response. Finally, it sends another request to the server with the newly created credential to complete the registration process.
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!
Allow your users to log in with a passkey
Implementing the login flow will be very similar to the passkey registration flow. Inside of /express-backend/services/passkey.js create two more functions startServerPasskeyLogin() and finishServerPasskeyLogin().
Now, similar to passkey registration create a handlePasskeyLogin() controller. Here, after the passkey login process is finished, the finishServerPasskeyLogin() returns JWT, which we decode using jose to get the User ID, verify it against the database and create a new session for the user.