Going passwordless with Passkeys: A guide for implementing passkeys in Next.js

Nblocks

Users, Subscriptions and Feature control
- All in one place

Try Nblocks for free

Welcome to an overview of Passkeys in web security. This article provides an insight into how Passkeys work and how they are becoming a preferred alternative to traditional password-based authentication. We will cover the fundamental concepts of Passkeys and offer a detailed guide on how to implement this technology in Next.js.

Join me and explore this inevitable next big thing in authentication!

The Future of Web Authentication: Moving Beyond Passwords

Introducing Passkeys: Easy to use and more secure

Passwords have been the approach all of us know very well, and secretly we all wish there was a better way. Throughout the years we have tried to patch its security flaws with password policies, One Time passwords (OTP), and making our users solve puzzles to prove they are human.

Passkeys is an authentication method that solves many issues with previous authentication methods. Everything is stored in a file called credential that is simple to use, works across multiple devices. You do not need to remember a password, you don’t even need to remember a username! All this and it’s more secure. Passkeys also remove the need for a second authentication step. No more redirecting to a text message with a 6 digit pin code. Finally we can go back to have a smooth login experience.

What Are Passkeys and How Do They Work?

Passkeys are a form of public-key cryptography used for user authentication. They leverage a pair of cryptographic keys - a private key stored securely on the user's device and a public key stored on the server. This method ensures that the actual authentication process happens locally on the user's device and not the other way around which significantly reduces the risk of credential theft or phishing attacks common in traditional password systems.

Why Passkeys Are Superior to Traditional Passwords

Unlike passwords, which are vulnerable to various security threats such as brute force attacks and phishing, Passkeys offer a much more secure alternative. They eliminate the need for users to remember and enter passwords, thereby reducing the risk of password-related breaches where passwords have been easy to guess. Additionally, Passkeys are unique for each website, meaning that a breach in one site doesn't compromise the user's identity on another, a common issue with reused passwords.

Diving Deeper into Passkeys Technology

The Mechanics Behind Passkeys: WebAuthn and Public Key Cryptography

Passkeys are built on the Web Authentication API (WebAuthn) standard, a core component of modern web security. WebAuthn allows servers to register and authenticate users using public key cryptography instead of a password. This technology not only enhances security but also paves the way for various user-friendly features like biometric authentication.

Enhanced Security Features of Passkeys

The security of Passkeys is fortified by their inherent design - they never leave the user's device, and the private key is never exposed to the server or potential attackers. This design, coupled with the fact that Passkeys do not require user interaction for each login (like typing a password), significantly reduces the surface for phishing attacks. Moreover, the use of biometrics or secure PINs on the user's device adds an additional layer of security, ensuring that only the legitimate user can authenticate using their Passkey.

Building with Passkeys: A Practical Guide for Next.js

This section outlines the process for integrating passkey authentication into a Next.js application. Our objective is to illustrate a clear path for setting up a passkey-based login system, focusing on fundamental concepts with minimal dependency on external libraries. This hands-on approach will help you grasp the fundamentals of passkey authentication.

Step-by-Step Implementation

Structure Overview

We begin by setting up the necessary components within our Next.js project. This involves creating two frontend views for user interaction

  1. one for registration (app/register/page.tsx)
  2. and another for login (app/login/page.tsx). 

These views serve as the entry points for users to interact with passkeys and the api endpoints.

On the backend, we'll establish two API endpoints: 

  1. pages/api/register.ts for handling new registrations 
  2. and pages/api/login.ts for managing login requests. 

These endpoints will be the backbone of our authentication flow, processing the registration and login operations.

A utility file, pages/api/utils.ts, will also be created to manage an in-memory database and perform cryptographic operations such as dealing with public keys and signing.

The Registration Flow

Passkeys WebAuthn registration flow diagram

The registration process involves several key steps to ensure secure handling and storage of user credentials:

  1. Initiating Registration: Users enter a user id in the registration view and request to register a new passkey.
  2. Challenge Generation by Backend: The backend generates a unique challenge and registration options for the user id, preparing for a secure credential exchange.
  3. Creating Passkey Credential: Using the options from the backend, the frontend creates a new passkey credential on the user's device, then uploads the attestation response to the backend.
  4. Verification and Storage by Backend: The backend verifies the attestation and the challenge. Upon successful verification, it stores the public key associated with the user for future logins.

The Login Flow

Passkeys WebAuthn login authentication flow diagram

The login flow simplifies the authentication process, leveraging passkeys to enhance security without compromising user convenience:

  1. User Login Request: Users input their user id in the login view and opt to log in using an existing passkey.
  2. Challenge Generation by Backend: The backend generates a unique challenge and login options tailored to the user id, ensuring secure authentication.
  3. Authentication with Passkey: The frontend lists available passkeys on the device. The user selects one, generating an authentication object that is sent back to the backend.
  4. Verification by Backend: The backend verifies the received authentication object against the stored public key and challenge, completing the authentication process if successful.

Emphasizing the Basics

Our goal is to demystify the concept of Passkeys, providing you with the knowledge to implement them from scratch. While libraries exist to streamline passkey integration, understanding the underlying principles is crucial. This guide focuses on the basics, using minimal dependencies to highlight the core functionalities of passkey authentication.

For a hands-on experience, I've hosted the complete code in an interactive StackBlitz playground. This setup not only allows you to view the code but also to interact with the running application in real-time. Explore the code, tweak it, and witness passkeys in action.

One clarification! Passkeys require high security. Some browsers and password plugins might not like that the project is running embedded like this. If you experience problems you can pull the code and run it locally or try another official example here.

Code Highlights and Explanation

The above code is a working example of Passkeys in action running on Next.js. Feel free to copy and play around with it as you like. 

There are some concepts and logic in the code to make everything work that is perhaps not as straightforward and demands some further explanation. Below I’ve listed a few things worth pointing out.

WebAuthn API Calls

Key operations in our frontend code are the calls to navigator.credentials.create and navigator.credentials.get. These are direct interactions with the WebAuthn API, responsible for generating and retrieving passkeys, respectively. The create function is used during the registration process to create a new passkey, while get is invoked for login, allowing the user to authenticate with an existing passkey. These API calls are pivotal for working with passkeys and are central to the code's functionality.

Verifying Signatures

The core logic for validating and authenticating passkeys lies within the verifyRegistration() and verifyLogin() methods found in utils.ts. During registration, we verify the signature of the authentication data using the public key extracted from the attestationObject. For login, we use the stored public key from a previous registration to validate the signature. These verification processes ensure that the passkey presented during login matches the one registered, affirming the authenticity of the user.

Base64 and ArrayBuffer Conversion

Throughout the code, you will notice several instances where data is converted between Base64 strings and ArrayBuffers. This conversion is critical when sending data over REST APIs because Web APIs and many web-related data formats prefer the use of ArrayBuffer for binary data manipulation. Base64 is a way to encode binary data into an ASCII string, which is more suitable for transmission over systems that only reliably support text. By converting to and from Base64, we ensure that binary data, like cryptographic keys and challenges, can be safely transmitted and received through HTTP requests.

Handling CBOR Data

The complexity of dealing with CBOR (Concise Binary Object Representation) data is simplified by using the cbor library. CBOR data, often encountered in WebAuthn for encoding attestation and authentication objects, is efficiently parsed using this library. Further, to make the cryptographic keys more manageable, we convert CBOR-encoded keys to JSON Web Key (JWK) format using the cose-to-jwk library. This conversion is crucial for working with web technologies and simplifies the handling of cryptographic keys without delving into the intricacies of COSE (CBOR Object Signing and Encryption) formats.

Storing and Comparing Challenges

Lastly, the implementation includes storing challenges upon their creation and comparing them with those provided during authentication. This step is vital for ensuring the integrity of the authentication process, preventing replay attacks, and verifying that the authentication request originates from a legitimate session.

How the authentication is configured, through WebAuth’s options

How passkeys can be created and then used for authentication can be configured through registerOptions and loginOptions and is part of the standard.

In my code example, the backend constructs both registerOptions and loginOptions objects that are passed back to the frontend. These are vital for configuring the WebAuthn API's behavior during the registration and authentication process. Each property of the registerOptions plays a specific role and here’s an overview:

  • challenge: A unique value provided by the server to prevent replay attacks.
  • rp: Represents the relying party (your service), with id specifying the domain name and name providing a human-readable name for your service.
  • user: Contains user-related information, including a unique id, a name (used as an identifier), and a displayName for presentation purposes.
  • pubKeyCredParams: Specifies the cryptographic parameters for the public key to be created, with alg indicating the algorithm (-7 for ES256) and type specifying the credential type.
  • timeout: The time in milliseconds the caller is willing to wait for the call to complete.
  • attestation: Specifies the attestation conveyance preference. "Direct" asks the authenticator to provide attestation information.
  • authenticatorSelection: Defines criteria for the authenticator. This includes the type of authenticator (platform for device-specific), the requirement for a resident key, and the level of user verification preferred.

Here’s an example object representing loginOptions that is given to the user before trying to login:

{
   "challenge": "uS7mKvaFnGHB4papgTU8K9IEPPBiA4WNhcpn5edWi0s=",
   "rp": {
       "id": "localhost",
       "name": "Example Service"
   },
   "user": {
       "id": "john.doe@example.com",
       "name": "John Doe",
       "displayName": "john.doe@example.com"
   },
   "pubKeyCredParams": [
       {
           "alg": -7,
           "type": "public-key"
       }
   ],
   "timeout": 60000,
   "attestation": "direct",
   "authenticatorSelection": {
       "authenticatorAttachment": "platform",
       "requireResidentKey": false,
       "userVerification": "preferred"
   }
}

Pitfalls and design considerations

Data formats

Because Webauthn uses ArrayBuffers and CBOR format it’s easy to make mistakes when serializing and deserializing data. Write tests that assure this is done right before spending hours understanding why a signing never succeds. 

Browser Limitations on Localhost

Developers may encounter issues where browsers refuse to sign or generate an attestation when working on localhost. This is a security measure by browsers to ensure the authenticity and security of the connection. To mitigate this, use HTTPS on localhost by leveraging tools like ngrok or consider setting up a development environment that mimics your production domain settings as closely as possible.

Selective Credential Listing with allowCredentials

Implementing the allowCredentials list when calling navigator.credentials.get can significantly refine the user experience by narrowing down the list of available passkeys. This makes it easier for users to select the correct passkey, especially on devices where multiple passkeys are stored. This approach requires careful management of user passkey ids on the server side to ensure the list is both accurate and secure.

Cross-Origin Concerns

Ensure that passkey requests are only entertained from trusted origins. Misconfiguration can lead to cross-origin attacks, where an attacker might trick a user into initiating authentication requests to your server from a malicious site.

UX considerations for existing application

When integrating passkeys into an existing application, prioritize a seamless user experience. Introduce passkeys as an enhanced security feature through clear, accessible guidance, helping users navigate the transition without confusion. For new users, present passkey registration as a primary option, emphasizing its benefits. Offer existing users a straightforward path to adopt passkeys, alongside their familiar login methods, to ensure comfort and confidence in the new system. Above all, maintain a flexible approach, allowing users to choose their preferred authentication method as they acclimate to passkeys.

Summarizing

When wrapping up this guide on passkeys, it's clear they mark a significant move towards making the web both safer and more user-friendly. Through a hands-on example with Next.js, we've explored their implementation, aiming to demonstrate that adopting passkeys is as much about embracing a secure future as it is about confronting the challenges that come with new technologies.

The example provided here is just a starting point. The true challenge—and reward—lies in applying these principles to your projects, learning from each hurdle, and contributing back to the community. We believe that Passkeys are the future of authentication and use this article as a reference and circle back to it anytime. That’s our idea when sharing our experiences with the community.

I hope this dive into passkeys has sparked your interest and provided the tools to experiment in your own work. Let’s continue to innovate responsibly, with an eye towards building a more secure, accessible web. Happy coding!

References and Further Reading

MDN WebAuthn Guide:
https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API

A demo of the WebAuthn specification
https://webauthn.io

W3C Web Authentication: An API for accessing Public Key Credentials - Level 2:
https://www.w3.org/TR/webauthn-2/

Google Developers Web Fundamentals - WebAuthn:
https://developers.google.com/web/updates/2018/05/webauthn

Yubico Developer Guide to WebAuthn:
https://developers.yubico.com/WebAuthn/

FIDO Alliance Resources:
https://fidoalliance.org/resources/

Share this post

Join the nblocks community

Unleash the power of nblocks powerful features today