An example of biometric authentication in web applications

A rather long and boring post describes an example of user authentication in web applications using biometrics (FaceID, fingerprint) built into mobile phones. Project code – hereworking demo – here. The example is written in pure JavaScript and can be debugged both on the backend (nodejs) and in the browser.

KDPV

KDPV

general review

To put it simply, there are two programs in a mobile phone – Browser (in which the web application runs) and Authenticator, as part of the mobile phone OS (Android or iPhone). The Authenticator can communicate with the mobile phone peripherals (camera and fingerprint scanner), and the Browser can communicate with the Authenticator (it is designed for this). Web Auth API).

Browser and Authenticator

Browser and Authenticator

The user sets up biometric authentication on their mobile phone (by fingerprint or FaceID), and the Authenticator generates asymmetric key pairs, which are then used to authenticate the user.

Attestation and Confirmation

The authentication process is divided into two steps:

  • Certification: The backend of the web application receives the public key generated in the mobile phone and stores it in the database, binding it to a specific user. Since a user can have more than one mobile phone, the binding of the phone (public key) to the user goes in a one-to-many ratio (one user – many public keys).

  • Confirmation (Assertion): to authenticate the user, the backend creates a challenge (challenge, a random sequence of bytes) and sends it to the front. The front browser asks the Authenticator to sign the received call with a secret key, after which it returns the signature to the back. The back verifies the signature using the public key stored earlier and authenticates the user.

Attestation and Confirmation

Attestation and Confirmation

Virtual Authenticator

The Chrome browser provides tools for developing web applications using the WebAuthn API. You can enable the virtual authenticator in “DevTools / Customize and control DevTools (triple-dots) / More tools / WebAuth”:

Enable WebAuth

Enable WebAuth

The authenticator panel will appear in the list of available panels. I moved this panel to the bottom, but by default it appears among the main toolbars available. In order for the virtual authenticator to work, you must first create it by selecting “Internal” as the transport (I have not experimented with other types):

WebAuthn panel

WebAuthn panel

After restarting the browser, you need to enable the virtual authenticator each time (Enable virtual authenticator environment). In this case, the previously created credentials (credentials) are lost.

Certification

Making a call

User attestation occurs either when he registers (in this case, the backend creates a new user for itself), or when a new device (smartphone) is connected to an existing account (the backend finds the user by his identifier – email, login, …).

In both cases, the backend must generate a unique challenge associated with a specific user and send it to the front:

A typical call looks something like this:

{
    "challenge": "O-SjwzNHvaJrIMBILj7vaupmbSXqaSpzhBiMaiXtq-w",
    "uuid": "user@email.com"
}

Obtaining a public key

After receiving a call from the back, the front creates a request to the Authenticator to obtain a public key:

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

The structure of the data passed to the Authenticator is something like this:

{
  "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"
    }
  }
}

The options specify the ECDSA (SHA-256) algorithm, its code – “-7”.

The authenticator authenticates the user in any available way (by fingerprint, via FaceID, using a graphic key, …) and returns to the browser (front) the user’s credentials, including his public key (AttestationObject):

AttestationObject

AttestationObject

Saving the public key on the back

The authenticator operates with binary data. To transfer them to the back, you need to encode binary data in some text format, for example Base64UrlEncoded:

{
  "cred": {
    "attestationId": "DDn8LhxnQB8g7qNKngMy-noDzSDIOyUMGg2soOeS6XA",
    "attestationObj": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikf6szqCHzhqRvtjZCcGybmpvrF7EKK4PpOd3KgOFc2VJFAAAAAQECAwQFBgcIAQIDBAUGBwgAIAw5_C4cZ0AfIO6jSp4DMvp6A80gyDslDBoNrKDnkulwpQECAyYgASFYIJ3Q9MQ0iOYg2HXVc6jO1wrIrmqhyOWAIu7G-QmMf9K0IlggF2qdOPRGQOPFyYOchDy-f2uqalA_NtSsk5Rqs85pN0U",
    "clientData": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiTFBJYlcyODdBZlVTeWRfNVlWUVJ4QjdSY1htVWY5Ym10NXBsNVZHbnllcyIsIm9yaWdpbiI6Imh0dHBzOi8vcGsuYXV0aC5kZW1vLnRlcWZ3LmNvbSIsImNyb3NzT3JpZ2luIjpmYWxzZX0"
  }
}

The most difficult thing is to extract the public key from the attestation data and store it in the database. For this I used two libraries:

I extracted the public key in JWK (JSON Web Key) format and saved it to the database as text:

{
  "kty": "EC",
  "alg": "ES256",
  "crv": "P-256",
  "x": "1rSQKqnG0I3uSLaUPsCqEzdHAqDWYWajw3UrPiy4BuI",
  "y": "KhXxXe5uJPlSSlYBADbA-rt38_FtyuVK0Jv3wTzgBlk"
}

The public key is tied to the ID of the passport (DDn…6XA), which is then used in the authentication confirmation (assertion).

Confirmation

After the public key of the user and the identifier of the passport are stored on the back and tied to the user identifier, an authentication confirmation (assertion) can be performed.

Making a call

In order to receive a challenge from the back, the front must somehow tell the back the identifier of the certificate with which the user’s public key is associated (in the example, I store the identifier in localStorage):

{"attestationId": "DDn8LhxnQB8g7qNKngMy-noDzSDIOyUMGg2soOeS6XA"}

The back generates a call and associates it with a public key bound to the passport identifier, after which it returns the call to the front.

{
  "attestationId": "DDn8LhxnQB8g7qNKngMy-noDzSDIOyUMGg2soOeS6XA",
  "challenge": "K5YhDdqmaBUVHfJFAi50EcmcLW2n08mLcvxMlsDVEGI"
}

Signature generation

The front requests the generation of a signature from the Authenticator, specifying the identity of the passport in the options:

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

Typical options structure:

{
  "publicKey": {
    "challenge": {/* binary */},
    "allowCredentials": [
      {
        "id": {/* binary */},
        "type": "public-key",
        "transports": [
          "internal"
        ]
      }
    ]
  }
}

The authenticator performs user authentication (by fingerprint, FaceID, etc.), after which it generates a digital signature using the private key and returns it to the browser as binary data:

AssertionObject

AssertionObject

Please note that the confirmation identifier (assertion.id) is the same as the certificate identifier (attestation.id) – “DDn8LhxnQB8g7qNKngMy-noDzSDIOyUMGg2soOeS6XA”.

Signature verification

Binary authentication confirmation data is encoded in text format and sent to the backend:

{
    "authenticatorData": "f6szqCHzhqRvtjZCcGybmpvrF7EKK4PpOd3KgOFc2VIFAAAAAg",
    "clientData": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiSXNpTGNpMlZaR1FHVE1KVlZVZTlONFI3WWt3bFd2WDFwZ1FaZDFaOTZoWSIsIm9yaWdpbiI6Imh0dHBzOi8vcGsuYXV0aC5kZW1vLnRlcWZ3LmNvbSIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
    "signature": "MEQCIBJjnmwRNzbE66R_CAdFiu2yklp4-Sindxxjxt8BUdL4AiB-0Mf7hd4t5jCk3ZDjAbcw-1DhLQQ0KHhhC0PSQaJQsA"
  }

At the back end, the data is converted back to binary format, and the client data containing the call is extracted from it:

{
  "type": "webauthn.get",
  "challenge": "R92jR_9v-33od9Yiea0RBWABjICbLjeQ1CXVBRo7X7M",
  "origin": "https://pk.auth.demo.teqfw.com"
}

Through a call, the user’s public key is found in the database and the electronic signature is verified.

Summary

Biometric authentication is not a replacement for password authentication, but can be a good addition to password authentication due to its ease of use by the end user. I think that the popularity of this method will grow (especially on mobile devices), despite the difficulties in implementation.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *