PublicKeyCredential in Web Apps

Publication date: 2023-05-19

In this post, I will provide an example of how to implement user authentication using PublicKeyCredential from the Web API. This is just-another-example from the internet that demonstrates the usage of this authentication method. You can try it out online yourself. Both parts of the example are written in pure JavaScript, and the source code can be found in the “flancer64/demo-webauthn-pubkey” repo (online demo).

Demo homepage

Common Scheme

First of all, it is necessary to present the general scheme of interaction among all actors involved: the User, the Authentication Device (Authenticator), the Browser, and the Backend with the Store:

Common scheme

All parts of the schema must be understood except for the Authenticator. An authenticator refers to a physical device or program that contains asymmetric encryption keys, which can be interacted with by a browser.

Virtual environment

A software authenticator is integrated into the Chrome browser for development purposes. You can find it by navigating to “DevTools / Customize and control DevTools (triple-dots) / More tools / WebAuth”.

Enable WebAuthn tools

I have placed the “WebAuthn” tab in the bottom panel, however, you can also find the “WebAuthn” tab in the top panel (by default):

WebAuthn panel in Chrome’s Dev Tools

Don’t forget to enable your authenticator by selecting “Enable virtual authenticator environment” and creating a new authenticator with the transport set to internal.

Attestation and Assertion

Attestation

Attestation is the process of registering a user’s public key with a backend:

Attestation

  1. The Browser requests an attestation challenge from the Backend (Relying Party).
  2. The Backend generates the attestation challenge and transmits it to the Browser.
  3. The Browser requests the Authenticator to either create new asymmetric keys or use existing ones, and generate attestation data accordingly.
  4. The User verifies their identity (using a PIN, fingerprint, FaceID, etc.).
  5. The Authenticator sends the attestation data to the Browser.
  6. The Browser sends the attestation data to the Backend.
  7. The Backend validates the attestation data using the attestation challenge and saves the public key to the Store.

Assertion

Assertion is the process of proving ownership of a registered credential during authentication:

Assertion

  1. The Browser requests an assertion challenge from the Backend (Relying Party).
  2. The Backend generates the assertion challenge and transmits it to the Browser.
  3. The Browser requests the Authenticator to sign the assertion challenge with the private key.
  4. The User verifies their identity (using a PIN, fingerprint, FaceID, etc.).
  5. The Authenticator signs the challenge and sends the assertion data to the Browser.
  6. The Browser sends the assertion data to the Backend.
  7. The Backend validates the assertion data using the stored public key from the Store.

Attestation Implementation

1. The attestation challenge request

In the request for an attestation challenge, the browser needs to provide a user identifier to allow the backend to associate the attestation challenge with the correct user. Typically, the user’s email is used as the user identifier. As a result, the initial step for attestation involves filling out a registration form, like the one shown below:

Sign Up

2. The attestation challenge generation

The backend registers a new user (or finds a previously registered user by his identifier) and generates a challenge for his certification — a random binary sequence 32 bytes long:

import {randomBytes} from ‘node:crypto’;
const challenge = randomBytes(32);

The attestation challenge is stored on the backend and must be unique among all other challenges to ensure its distinctiveness.

Since the attestation challenge needs to be transmitted over the internet, it should be base64 URL encoded. Here is the typical data that the backend returns to the browser for an attestation challenge request:

{

“challenge”: “O-SjwzNHvaJrIMBILj7vaupmbSXqaSpzhBiMaiXtq-w”,

“uuid”: “user@email.com”

} * Demo_Back_Web_Api_Sign_Up * Fl32_Auth_Back_Mod_PubKey.attestChallengeCreate

3. The attestation data request

After the browser receives an attestation challenge, it can send a request to the authenticator to retrieve the attestation data using the following code:

/** @type {PublicKeyCredential} */
const attestation = await navigator.credentials.create({publicKey});

Here is a typical structure for the request:

{

“publicKey”: {

“rp”: {

“name”: “WebAuthn Demo”

},

“user”: {

“id”: {/* binary data */},

“name”: “user@email.com”,

“displayName”: “user@email.com”

},

“challenge”: {/* binary data */},

“pubKeyCredParams”: [

{

“type”: “public-key”,

“alg”: -7

}
],

“timeout”: 300000,

“authenticatorSelection”: {

“authenticatorAttachment”: “platform”,

“userVerification”: “preferred”

}
}
} * Demo_Front_Ui_Route_Sign_Up * Fl32_Auth_Front_Mod_PubKey.composeOptPkCreate

4. User verification

The authenticator prompts the user to verify their identity using a specific method, such as fingerprint authentication:

User verification with fingerprint

5. The attestation data

To ensure independence from the usage environment, the attestation data is designed to be binary. The following approximate data is transmitted from the authenticator to the browser:

The attestation data

6. Transfer of attestation data to the backend

The attestation data needs to be encoded in base64 URL format before sending it to the backend:

{

“cred”: {

“attestationId”: “DDn8LhxnQB8g7qNKngMy-noDzSDIOyUMGg2soOeS6XA”,

“attestationObj”: “o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikf6szqCHzhqRvtjZCcGybmpvrF7EKK4PpOd3KgOFc2VJFAAAAAQECAwQFBgcIAQIDBAUGBwgAIAw5_C4cZ0AfIO6jSp4DMvp6A80gyDslDBoNrKDnkulwpQECAyYgASFYIJ3Q9MQ0iOYg2HXVc6jO1wrIrmqhyOWAIu7G-QmMf9K0IlggF2qdOPRGQOPFyYOchDy-f2uqalA_NtSsk5Rqs85pN0U”,

“clientData”: “eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiTFBJYlcyODdBZlVTeWRfNVlWUVJ4QjdSY1htVWY5Ym10NXBsNVZHbnllcyIsIm9yaWdpbiI6Imh0dHBzOi8vcGsuYXV0aC5kZW1vLnRlcWZ3LmNvbSIsImNyb3NzT3JpZ2luIjpmYWxzZX0”

}
} * Demo_Front_Ui_Route_Sign_Up * Fl32_Auth_Front_Mod_PubKey.attest * Fl32_Auth_Back_Web_Api_Attest

7. Validation of the attestation data

This is the crucial step of the process — the attestation data is validated on the backend, and the user’s public key is associated with the corresponding user. The most challenging aspect is accurately parsing and presenting the attestation data. In my demo application, I utilized the following libraries for this purpose:

Ultimately, I extract the user’s public key from the attestation data and store it in the database in JWK (JSON Web Key) format:

{

“kty”: “EC”,

“alg”: “ES256”,

“crv”: “P-256”,

“x”: “1rSQKqnG0I3uSLaUPsCqEzdHAqDWYWajw3UrPiy4BuI”,

“y”: “KhXxXe5uJPlSSlYBADbA-rt38_FtyuVK0Jv3wTzgBlk”

} On the frontend, the attestation ID (DDn…6XA) is stored in local storage for later use in assertions.

Assertion Implementation

1. The assertion challenge request

In the demo application, each user can have only one attestation associated with a public key. Therefore, the request specifies the ID of this particular attestation:

{“attestationId”: “DDn8LhxnQB8g7qNKngMy-noDzSDIOyUMGg2soOeS6XA”} * Demo_Front_Ui_Route_Sign_In * Fl32_Auth_Front_Mod_PubKey.assertChallenge * Fl32_Auth_Back_Web_Api_Assert_Challenge

2. The assertion challenge generation

The backend retrieves the corresponding attestation from its database, generates an assertion challenge, and associates it with the located attestation:

{

“attestationId”: “DDn8LhxnQB8g7qNKngMy-noDzSDIOyUMGg2soOeS6XA”,

“challenge”: “K5YhDdqmaBUVHfJFAi50EcmcLW2n08mLcvxMlsDVEGI”

} * Fl32_Auth_Back_Web_Api_Assert_Challenge * Fl32_Auth_Back_Mod_PubKey.assertChallengeCreate

3. The assertion data request

After the browser receives an assertion challenge, it can send a request to the authenticator to retrieve the assertion data using the following code:

/** @type {PublicKeyCredential} */
const assertion = await navigator.credentials.get({publicKey});

Here is a typical structure for the request:

{

“publicKey”: {

“challenge”: {/* binary */},

“allowCredentials”: [

{

“id”: {/* binary */},

“type”: “public-key”,

“transports”: [

“internal”

]
}
]
}
} * Demo_Front_Ui_Route_Sign_In * Fl32_Auth_Front_Mod_PubKey.composeOptPkGet

4. User verification

Similar to the attestation data request, the authenticator prompts the user to verify their identity.

5. The assertion data

The authenticator provides binary data to the browser for user assertion:

The assertion data

Please note that the assertion.id should be the same as the attestation.id (DDn8LhxnQB8g7qNKngMy-noDzSDIOyUMGg2soOeS6XA).

6. Transfer of assertion data to the backend

Just like attestation data, the assertion data needs to be encoded in base64 URL format before sending it to the backend:

{

“authenticatorData”: “f6szqCHzhqRvtjZCcGybmpvrF7EKK4PpOd3KgOFc2VIFAAAAAg”,

“clientData”: “eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiSXNpTGNpMlZaR1FHVE1KVlZVZTlONFI3WWt3bFd2WDFwZ1FaZDFaOTZoWSIsIm9yaWdpbiI6Imh0dHBzOi8vcGsuYXV0aC5kZW1vLnRlcWZ3LmNvbSIsImNyb3NzT3JpZ2luIjpmYWxzZX0”,

“signature”: “MEQCIBJjnmwRNzbE66R_CAdFiu2yklp4-Sindxxjxt8BUdL4AiB-0Mf7hd4t5jCk3ZDjAbcw-1DhLQQ0KHhhC0PSQaJQsA”

} * Demo_Front_Ui_Route_Sign_In * Fl32_Auth_Front_Mod_PubKey.validate * Fl32_Auth_Back_Web_Api_Assert_Validate

7. Validation of the assertion data

First, you need to transform the clientData into a regular object:

{

“type”: “webauthn.get”,

“challenge”: “R92jR_9v-33od9Yiea0RBWABjICbLjeQ1CXVBRo7X7M”,

“origin”: “https://pk.auth.demo.teqfw.com”

} Then, retrieve the user’s public key from the database for the corresponding challenge (refer to step 2).

The final step is to verify the correctness of the data signature using the user’s public key stored in the database.

Conclusion

The primary objective of this post is to showcase functional code that enables the utilization of public key user authentication. Assembling the various fragments describing authentication posed some challenges, given the range of potential use cases. To gain a comprehensive understanding of authentication, it is recommended to deploy the demo application locally and step through all the stages using a debugger for both the frontend and backend components.

If you enjoyed this article, please give it a clap and follow me for more content!

Stay connected:

Thank you for your support!