DEV Community

Cover image for The future of web authentication using touch / face / PIN is there!
Arnaud Dagnelies
Arnaud Dagnelies

Posted on • Edited on • Originally published at blog.passwordless.id

The future of web authentication using touch / face / PIN is there!

Introduction

Web authentication through local device authentication like Android Touch ID, Windows Hello Face recognition or just a local device PIN code. This is now available for Browsers and offers not only a smoother user experience but is also more secure.

It has many names: namely "Passwordless", "WebAuthn" and "PassKeys" but they all revolve around the same. It leverages a recent browser protocol called WebAuthn which acts as a glue to invoke the local device authentication mechanism. Before going into details, let's see what it looks like (with my laptop/phone using a German locale).

Looks great, right? Do you know anyone who likes passwords? Me neither. No more passwords == better UX. All those complex password rules, not reusing them, etc. is just annoying. And in the end, you forget the password anyway and must reset it.

Getting rid of passwords makes any registration/authentication much smoother and quicker. Moreover, it has several other benefits.


More secure

Better than passwords

It is two-factor authentication in a single step.

  • The first factor is something you have. The authentication only works on registered devices.

  • The second factor is something you are, or something you know. A biometric verification, or the device PIN code, is required.

A combination of both is required to authenticate the user.

It protects from phishing

Phishing usually involves the user typing their password into a fake website. It is the most common way to hack accounts.

By getting rid of passwords, you get rid of phishing! Great, right?

Moreover, it also protects against further security threats like password reuse, social engineering attempts and the like.

It protects against server breaches

The underlying protocol relies on asymmetric cryptography. The private key is stored on the device while only the public key is known by the server. Even if the clear text data on the server is stolen, it is not sufficient to authenticate the user. The user's device platform itself must be hacked to do so.


Privacy oriented

Your fingerprint/face never leaves your device

Biometric verification uses the local authentication mechanism from your device. On the device, this biometric information is strongly protected and never exposed.

Verification is a safety measure used to prove you are you, then create or access cryptographic keys stored on your device. These keys, also known as passkeys, are in turn used for the authentication mechanism.

You can choose not to use fingerprint/face

The user verification is delegated to your platform. If you are uncomfortable with such a mechanism, you can still use a PIN, a password, a swipe pattern configured, or whatever is configured on your device as the local authentication mechanism.

You are anonym

The protocol does not reveal anything about you. It just receives randomly generated key pair IDs and public keys. Moreover, each website has its own set of credentials and has no idea about the credentials used by other websites.


How does it work exactly?

Introduction

The authentication relies on a recent browser protocol called webauthn which is based on asymmetric cryptography.

During registration on a website, a pair of public and private keys is generated for the user. The private key is stored on the userโ€™s device, while the public key is sent to the server.

When a user wants to authenticate themselves, they first request a challenge from the server. Then, they will reply with a message containing this challenge and signed using their private key.

The signature can lastly be verified by the server using the userโ€™s public key obtained during registration. If the signature matches (and some other properties), the user is authenticated.

This simplified explanation of the protocol is illustrated by the following diagram.

Show me the code!

To provide meaningful examples while avoiding the overhead of the native protocol, the following sections will make use of the @passwordless-id/webauthn library, which is a high-level wrapper around the protocol.

It can be installed via npm install @passwordless-id/webauthn or imported directly in browser module scripts using import { client } from 'https://unpkg.com/@passwordless-id/webauthn'

Registration (browser part)

For registration, a cryptographic key pair must be generated for the user, it will be called a "credential" in the WebAuthn terminology. It can be created using the following call.

import { client } from '@passwordless-id/webauthn' 

const registration = await client.register("MyUsername", "some-random-generated-by-server")
Enter fullscreen mode Exit fullscreen mode

For further options, check out the library's docs. Once the user confirms it using biometrics or PIN code, the key pair will be created, and the function will return. The private key is stored on the device, protected by local authentication, while the public key is returned.

{
  "username": "MyUsername",
  "credential": {
    "id": "3924HhJdJMy...",
    "publicKey": "...",
    "algorithm": "ES256"
  },
  "authenticatorData": "...",
  "clientData": "..."
}
Enter fullscreen mode Exit fullscreen mode

This is a plain JSON object to be sent to the server. It contains various base64url encoded data that will first have to be parsed and verified.

Registration (server side)

Since the library also handles the server side, it can be used too there.

import { server } from '@passwordless-id/webauthn' 

const expected = {
    challenge: "whatever-was-randomly-generated-by-the-server",
    origin: "http://localhost:8080",
}
const registrationParsed = await server.verifyRegistration(registration, expected)
Enter fullscreen mode Exit fullscreen mode

Either this operation fails and throws an Error, or the verification is successful and returns the parsed registration. Example result:

{
  "username": "Arnaud",
  "credential": {
    "id": "3924HhJdJMy...",
    "publicKey": "...",
    "algorithm": "ES256"
  },
  "authenticator": {
    ...
    "name": "Windows Hello Hardware Authenticator"
  },
  ...
}
Enter fullscreen mode Exit fullscreen mode

The registration is now considered complete.

What must be stored server side is:

  • The credential (id, publicKey, algorithm)

  • Associate the credential.id to the user

When the user wants to authenticate later, you will need the public key and algorithm to verify the signature. It is imperative to store it. In contrast, the authenticator part is optional. It delivers information about the authenticating device and is not verified.

Authentication (browser side)

When a user wants to authenticate themselves, they must sign a random "challenge" using their private key. This signature is sent to the server, which verifies it using the user's public key.

import { client } from 'webauthn'

const authentication = await webauthn.authenticate(["allowed-credential-id", "other-allowed-credential-id"], "random-server-challenge")
Enter fullscreen mode Exit fullscreen mode

Again, for all the available options, just consult the library's documentation. Once the user confirms the authentication attempt through biometric or PIN verification, the method returns the result.

{
  "credentialId": "3924HhJdJMy...",
  "authenticatorData": "...",
  "clientData": "...",
  "signature": "..."
}
Enter fullscreen mode Exit fullscreen mode

Unlike the registration, there is no credential object, just the credentialId. This JSON can be sent as it is to the server.

Please note that the username is not present. It is not part of the WebAuthn authentication protocol. Therefore, a mapping credentialId -> username might be particularly useful to maintain server side. Alternatively, inject username in the JSON object before sending it.

Authentication (server side)

The most important parts now are to:

  • Verify the challenge

  • Verify the signature

  • Validate a few more "details"

Given the credentialId, the server should possess the corresponding publicKey and algorithm used. These will be needed to verify the signature. The authentication verification procedure can be done with a single call.

import { server } from '@passwordless-id/webauthn' 

// obtained from database by looking up `authentication.credentialId`
const credentialKey = { 
    id: "3924HhJdJMy...",
    publicKey: "...",
    algorithm: "ES256"
}

const expected = {
    challenge: "whatever-was-generated-server-side",
    origin: "http://localhost:8080",
    userVerified: true,
    counter: 0 
}
Enter fullscreen mode Exit fullscreen mode

Again, consult the documentation for all parameters. For example, a function predicate can be used for challenge and origin to make the verification more dynamic.

const authenticationParsed = await server.verifyAuthentication(authentication, credentialKey, expected)
Enter fullscreen mode Exit fullscreen mode

Either this operation fails and throws an Error, or the verification is successful and returns the parsed authentication payload.

Please note that this parsed result authenticationParsed has no real use. It is solely returned for the sake of completeness. The verifyAuthentication already verifies the payload, including the signature.

Important final remarks

While it looks like an easy replacement for password based authentication. A bit more thought should be put into it.

Unlike traditional authentication systems that can be accessed from anywhere using a single password, authentication here is device bound. Losing a device means losing the private key used to sign in.

That the authentication is device bound affects many aspects of the user authentication:

  • anonymous "usernameless" authentication becomes possible (where each device is its own account)

  • by default, relying on a single registered device also implies losing access completely if the device is lost/broken/stolen

  • registering multiple devices and being able to modify the list of allowed devices should be considered

  • more diverse account recovery options become possible, like through another registered device

Taking care of all these aspects should not be underestimated. Especially if the goal is to replace traditional password based user authentication in an existing system.


Passwordless.ID

The vision

Passwordless authentication comes with its fair share of complexity and gotchas. "Passwordless.ID" aims to solve this by being a "free public identity provider".

Its philosophy is simple.

  • Make the web a safer place

  • Make it easier for developers

  • More comfort and control for users

This is achieved by providing tools and services to delegate the authentication to the Passwordless.ID API.

OAuh2 / OpenID compatible

Passwordless.ID is compatible with both OAuth2 and OpenID protocols. That way, you can use it as a generic OpenID provider for a "Sign in with..." button.

If you are familiar with OAuth, you probably know that it is an "authorization" protocol. Usually, the API also offers a set of operations to grant permission to. In the case of Passwordless.ID, the only operation is accessing (part of) the user profile.

If you want to add Passwordless.ID as an additional social login provider using some predefined library, check out our OAuth2/OpenID guide!

Sign in with...

For a straightforward and smooth integration, you can use the @passwordless-id/connect library. This library makes it possible to trigger the authentication/authorization using a single call.

Check out the standalone demo to try it out and the user guide for implementing this on your site in 5 minutes! Not even an account is necessary!

Shared Profile

Why would you need to register your profile information, portrait, address over and over for every website or app? It is simply annoying for everyone. Using this shared profile, any website or app can request read access to it. That way, the user has a single place to maintain the profile up to date.

The same goes for the list of authorized devices. It simply makes more sense to maintain it in one place.

Free and public

Passwordless.ID is free for everyone and forever without a catch. It is our conviction that making it publicly available is the best way to make the internet a safer place as a whole.

To keep it striving in the long term, some funding is crucial. We would love to find some sponsors. If you are interested, please contact us or just drop a comment here.

Empowering the users

Using Passwordless.ID, the privacy and security settings will be in the hands of the users instead of the websites and apps. The users will choose whether they stay logged in or not, what account recovery mechanisms they deem safe enough, what information they agree to share without explicit consent, and so on.

In particular, the last point is interesting. For example, a user might agree to share its nickname and portrait publicly. That way, any website could show its avatar in the corner without explicit authorization request beforehand.

Currently in development

Passwordless.ID is not yet "finished". It is currently at the early proof of concept stage.

Although it lacks capabilities, it can already be used for testing and integration purposes. However, please note that the database might be reset at any point in the future until further notice.


We need you!

Any feedback is welcome

This was made with love, sweat and considerate thoughts. We strive to make the best possible authentication experience and are glad to hear any feedback. Even a little "like" on this article is encouraging.

Use it

This is meant to be a public identity provider. Help us make the web a better and safer place while making your developer's life easier. The best way is to use it. If you need help, just post an issue. And if you already integrated it, we would be glad to feature you in a future blog article!

Wanna write about it?

In case you plan to write a blog article, a tutorial, some news or anything alike, we would be glad to hear from you. Here are some logos and banners if you want. We might feature it on our blog too!

Share it

If you like it too, talk about it to others! Share it with someone! Every little act is of helps make it succeed. Thank you!

Some further links:

Top comments (3)

Collapse
 
dagnelies profile image
Arnaud Dagnelies

So, is this useful for anyone?

Collapse
 
raphaelhs profile image
raphaelhs

Hi Arnoud, I'm using your library for the second authentication factor in a web solution.

The integration of WebAuthn development and testing into our solution, but I am having difficulty restricting the authentication method. I would need to restrict the method to Biometrics only.

In my research, I found that there are WebAuthn extensions, such as "UVM" that handle this. However, I didn't find anything related to the use of these extension methods in your library.

Can you help me if this functionality is expected, if there is any way to use it together with your library, or if I should use another library to be able to manipulate these extensions.

I really liked your work because it has excellent and detailed documentation, congratulations on that. Thanks..

Collapse
 
dagnelies profile image
Arnaud Dagnelies • Edited

While there are extensions like you mentioned, I don't really recommend it. The first reason is that it constraints the user into specific authentication methods that the user may dislike. For example, with a laptop without sensors having only PIN or a phone with swipe pattern because the user prefers it that way. The other issue is that it's 'extensions', as such the support is optional and I also believe is not included as part of the signed assertion... Although I'm uncertain regarding this last statement.

It's not supported by the lib. A PR for this feature and some experimentation is welcome, but not really my priority right now.