How we built the Chrome DevTools WebAuthn tab

Published on

The Web Authentication API, also known as WebAuthn, allows servers to use public key cryptography - rather than passwords - to register and authenticate users. It does this by enabling integration between these servers and strong authenticators. These authenticators may be dedicated physical devices (e.g. security keys) or integrated with platforms (e.g. fingerprint readers). You can read more about WebAuthn here at

Developer pain points #

Prior to this project, WebAuthn lacked native debugging support on Chrome. A developer building an app that used WebAuth required access to physical authenticators. This was especially difficult for two reasons:

  1. There are many different flavors of authenticators. Debugging the breadth of configurations and capabilities necessitated that the developer have access to many different, sometimes expensive, authenticators.

  2. Physical authenticators are, by design, secure. Therefore, inspecting their state is usually impossible.

We wanted to make this easier by adding debugging support right in the Chrome DevTools.

Solution: a new WebAuthn tab #

The WebAuthn DevTools tab makes debugging WebAuthn much easier by allowing developers to emulate these authenticators, customize their capabilities, and inspect their states.

New WebAuthn tab

Implementation #

Adding debugging support to WebAuthn was a two-part process.

Two-part process

Part 1: Adding WebAuthn Domain to the Chrome DevTools Protocol #

First, we implemented a new domain in the Chrome DevTools Protocol (CDP) which hooks into a handler that communicates with the WebAuthn backend.

The CDP connects DevTools front-end with Chromium. In our case, we utilized the WebAuthn domain acts to bridge between the WebAuthn DevTools tab and Chromium's implementation of WebAuthn.

The WebAuthn domain allows enabling and disabling the Virtual Authenticator Environment, which disconnects the browser from the real Authenticator Discovery and plugs in a Virtual Discovery instead.

We also expose methods in the domain that act as a thin layer to the existing Virtual Authenticator and Virtual Discovery interfaces, which are part of Chromium's WebAuthn implementation. These methods include adding and removing authenticators as well as creating, getting, and clearing their registered credentials.

(Read the code)

Part 2: Building the user-facing tab #

Second, we built a user-facing tab in the DevTools frontend. The tab is made up of a view and a model. An auto-generated agent connects the domain with the tab.

While there are 3 necessary components needed, we only needed to be concerned about two of them: the model and the view. The 3rd component - the agent, is autogenerated after adding the domain. Briefly, the agent is the layer that carries the calls between the front end and the CDP.

The model #

The model is the JavaScript layer that connects the agent and the view. For our case, the model is quite simple. It takes commands from the view, builds the requests such that they're consumable by the CDP, and then sends them through via the agent. These requests are usually one-directional with no information being sent back to the view.

However, we do sometimes pass back a response from the model either to provide an ID for a newly-created authenticator or to return the credentials stored in an existing authenticator.

(Read the code)

The view #

New WebAuthn tab

We use the view to provide the user-interface that a developer can find when accessing DevTools. It contains:

  1. A toolbar to enable virtual authenticator environment.
  2. A section to add authenticators.
  3. A section for created authenticators.

(Read the code)

Toolbar to enable virtual authenticator environment #

Since most of the user-interactions are with one authenticator at a time rather than the entire tab, the only functionality we provide in the toolbar is toggling the virtual environment on and off.

Why is this necessary? It's important that the user has to explicitly toggle the environment because doing so disconnects the browser from the real Authenticator Discovery. Therefore, while it's on, connected physical authenticators like a fingerprint reader won't be recognized.

We decided that an explicit toggle means a better user experience, especially for those who wander into the WebAuthn tab without expecting real discovery to be disabled.

Adding the Authenticators section #
Adding the Authenticators section

Upon enabling the virtual authenticator environment, we present the developer with an inline-form that allows them to add a virtual authenticator. Within this form, we provide customization options which allow the user to decide the authenticator's protocol and transport methods, as well as whether the authenticator supports resident keys and user verification.

Once the user clicks Add, these options are bundled and sent to the model which makes the call to create an authenticator. Once that's complete, the front end will receive a response and then modify the UI to include the newly-created authenticator.

The Authenticator view #
The Authenticator view

Each time an authenticator is emulated, we add a section to the authenticator view to represent it. Each authenticator section includes a name, ID, configuration options, buttons to remove the authenticator or set it active, and a credential table.

The Authenticator name #

The authenticator's name is customizable and defaults to "Authenticator" concatenated with the last 5 characters of its ID. Originally, the authenticator's name was its full ID and unchangeable. We implemented customizable names so the user can label the authenticator based on its capabilities, the physical authenticator it's meant to emulate, or any nickname that's a bit easier to digest than a 36-character ID.

Credentials table #

We added a table to each authenticator section that shows all the credentials registered by an authenticator. Within each row, we provide information about a credential, as well as buttons to remove or export the credential.

Currently, we gather the information to fill these tables by polling the CDP to get the registered credentials for each authenticator. In the future, we plan on adding an event for credential creation.

The Active button #

We also added an Active radio button to each authenticator section. The authenticator that is currently set active will be the only one that listens for and registers credentials. Without this, the registering of credentials given multiple authenticators is non-deterministic which would be a fatal flaw when trying to test WebAuthn using them.

We implemented the active status using the SetUserPresence method on virtual authenticators. The SetUserPresence method sets whether tests of user presence succeed for a given authenticator. If we turn it off, an authenticator won't be able to listen for credentials. Therefore, by ensuring that it is on for at most one authenticator (the one set as active), and disabling user presence for all the others, we can force deterministic behavior.

An interesting challenge we faced while adding the active button was avoiding a race condition. Consider the following scenario:

  1. User clicks Active radio button for Authenticator X, sending a request to the CDP to set X as active. The Active radio button for X is selected, and all the others are deselected.

  2. Immediately after, user clicks Active radio button for Authenticator Y, sending a request to the CDP to set Y as active. The Active radio button for Y is selected, and all the others, including for X, are deselected.

  3. In the backend, the call to set Y as active is completed and resolved. Y is now active and all other authenticators are not.

  4. In the backend, the call to set X as active is completed and resolved. X is now active and all other authenticators, including Y, are not.

Now, the resulting situation is as follows: X is the active authenticator. However, the Active radio button for X isn't selected. Y isn't the active authenticator. However, the Active radio button for Y is selected. There is a disagreement between the front end and the actual status of the authenticators. Obviously, that's a problem.

Our solution: Establish pseudo two-way communication between the radio buttons and the active authenticator. First, we maintain a variable activeId in the view to keep track of the ID of the currently active authenticator. Then, we wait for the call to set an authenticator active to return then set activeId to the ID of that authenticator. Lastly, we loop through each authenticator section. If the ID of that section equals activeId, we set the button to selected. Otherwise, we set the button to unselected.

Here's what that looks like:

async _setActiveAuthenticator(authenticatorId) {
await this._clearActiveAuthenticator();
await this._model.setAutomaticPresenceSimulation(authenticatorId, true);
this._activeId = authenticatorId;

_updateActiveButtons() {
const authenticators = this._authenticatorsView.getElementsByClassName('authenticator-section');
Array.from(authenticators).forEach(authenticator => {
authenticator.querySelector('input.dt-radio-button').checked =
authenticator.getAttribute('data-authenticator-id') === this._activeId;

async _clearActiveAuthenticator() {
if (this._activeId) {
await this._model.setAutomaticPresenceSimulation(this._activeId, false);
this._activeId = null;

Usage metrics #

We wanted to track this feature's usage. Initially, we came up with two options.

  1. Count each time the WebAuthn tab in DevTools was opened. This option could potentially lead to overcounting, as someone may open the tab without actually using it.

  2. Track the number of times the "Enable virtual authenticator environment" checkbox in the toolbar was toggled. This option also had a potential overcounting problem as some may toggle the environment on and off multiple times in the same session.

Ultimately, we decided to go with the latter but restrict the counting by having a check to see if the environment had already been enabled in the session. Therefore, we would only increase the count by 1 regardless of how many times the developer toggled the environment. This works because a new session is created each time the tab is reopened, thus resetting the check and allowing for the metric to be incremented again.

Summary #

Thank you for reading! If you have any suggestions to improve the WebAuthn tab, let us know by filing a bug.

Here're some resources if you would like to learn more about WebAuthn:

Last updated: Improve article

We serve cookies on this site to analyze traffic, remember your preferences, and optimize your experience.