Webauthn - Login/Authentication

Arnaud Dagnelies - Dec 13 '22 - - Dev Community

Somehow, I missed this part in the webauthn protocol series. Originally, I wrote the introduction, with the flows and so on but this crucial part, about the authentication was missing. Perhaps due to the lack of interest, I never finished it. Here it comes, better late than never!

You can also try out everything at passwordless.id or use the simplifying library or just play around with the playground to try out the options.

The authentication has similar steps to the registration, and depicted in the sequence diagram below.

authentication-sequence-diagram

1. Request a challenge from the server

The challenge is basically a nonce to ensure the authentication payload cannot be reused in replay attacks. It is just a bunch of bytes randomly generated from the server.

2. Trigger the webauthn protocol in the browser side

This is done like this:

let assertion = await navigator.credentials.get({
  publicKey: {
    challenge: Uint8Array.from(
        randomStringFromServer, c => c.charCodeAt(0)),
    allowCredentials: [{
        id: Uint8Array.from(
            credentialId, c => c.charCodeAt(0)),
        type: 'public-key',
        transports: ['internal', 'hybrid','usb', 'ble', 'nfc'],
    }],
    timeout: 60000,
}
});
Enter fullscreen mode Exit fullscreen mode

It's interesting to note that there are two possible usages / behaviors here:

  1. Providing allowCredentials with the list of credential IDs allowed
    • typically fetched alongside the challenge from the server for a given username
  2. Leaving allowCredentials as an empty list
    • a UI will pop up to let you choose a credential you already registered here
    • how the UI looks is platform specific, quite recent, still evolving, and a bit buggy from my experience

The transports defines how the user can authenticate. internal is to let the device use the local authentication method, hybrid is authentication on a desktop computer using a smartphone for example and let the platform decide how, usb for security keys, ble for bluetooth and nfc for NFC.

Once the user "authenticates" locally, using biometrics like fingerprint, swiping a pattern or whatever the platform offers, the method returns.

Again, what you obtain is not a JSON like structure, but an object filled with ArrayBuffers and a few methods.

It looks like this:

{
   authenticatorAttachment: null,
   id: "IJeOSAbRJ4FNTS1aF5D...",
   rawId: ArrayBuffer(32),
   type: "public-key",
   response: {
     authenticatorData: ArrayBuffer(37)
     clientDataJSON: ArrayBuffer(138)
     signature: ArrayBuffer(71)
     userHandle: ArrayBuffer(32)
}
Enter fullscreen mode Exit fullscreen mode

Again, you cannot just send it like this to the server, but you must first encode it in some JSON, usually by base64url-encoding these ArrayBuffers.

When "decoding" these array buffers, which are kind of low-level structures, you will obtain various information that you must validate. Most notably the signature, which proves that the provided information is authentic, that the one sending the payload is in possession of the private key.

Roughly speaking, the authenticatorData which is a sequence of bytes provides following information:

{
  "rpIdHash": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2M=",
  "flags": {
    "userPresent": true,
    "userVerified": true,
    "backupEligibility": false,
    "backupState": false,
    "attestedData": false,
    "extensionsIncluded": false
  },
  "counter": 1
}
Enter fullscreen mode Exit fullscreen mode

The clientData is simpler, it is just a UTF-8 string representing raw JSON, and decoded looks like this:

{
  "type": "webauthn.get",
  "challenge": "24d224d3-1d0f-4301-8759-398870585e55",
  "origin": "http://localhost:8080",
  "crossOrigin": false
}
Enter fullscreen mode Exit fullscreen mode

Both of these payloads have to be verified for their validity and the signature too using the public key and signature algorithm obtained during registration.

The "data" that is signed is the authenticatorData byte array concatenated with the sha256 hash of clientData.

All this scratches the surface of this complex protocol. Therefore I strongly advise you to use some library and not to roll your own. It will save you some headaches. ;)

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .