devnull.land

Miscellaneous bits and bobs from the tech world

Node.js FIDO2/WebAuthn Pitfalls and Solutions

1/12/2022, 10:03:20 PM


While implementing what was supposed to be a rather straightforward use-case of second-factor authentication via WebAuthn, I ran into a surprising amount of roadblocks that made my implementation harder than it should've been. Complicating the procedure was that I initially implemented the hardware key checking using the old deprecated U2F protocol — which for the record, was more straightforward.

However, WebAuthn is the future — just look how slick this guide is! If that's not going to make you think it'll be done lickity-split, I don't know what will. Reading through that helpful guide (put together by the fine folks at DUO), I couldn't help but be struck by how "enterprise-y" it all felt. It did not bode well, and my fears were not unfounded.

I decided to put this post together as a catch-all for the problems I faced in my implementation, and direct links or write-ups to the solutions.


MDN docs, what MDN docs?

My first stop was MDN. Their guides are simple and straightforward, and if I wanted to get going as easily as possible, this was where I'd go. However, the introductory guide is very dense and does not contain any implementable code.

Solution

On the server-side, use fido2-lib, maintained by Adam Powers. It is maintained under an open-source GitHub organization, and it contains all of the backend nitty-gritty server-side code, so all you need to do is wire up your route handlers and library defaults, and hand it all off to this library.

CBOR Encoding

What encoding now? This issue contains a detailed Q&A about why CBOR encoding is used in the WebAuthn flow.

To you as the end-to-end dev, it means one thing: It is impossible to hook up the outputs of fido2-lib and the client-side Navigator.credentials interface because the outputs of one cannot be sent over the wire in its native format (A Uint8Array buffer).

I'd get cryptic, unhelpful errors like TypeError: CredentialsContainer.create: Missing required 'challenge' member of PublicKeyCredentialCreationOptions..

I spent far too long trying to figure out the encoding, trying various libraries to finangle it into some sort of format I could send over the wire.

Solution

Use github/webauthn-json. It lets you call navigator.credentials.create() and navigator.credentials.get() and pass in base64url strings instead of a Uint8Array. Sorted.

base64url?

Yeah, not base64. base64url.

Solution

Use brianloveswords/base64url and atob.

If you want to turn a Uint8Array into base64url:

const yourBase64Url = base64url(yourUint8Array);

... and in reverse (because fido2-lib expects some responses from the client as a Uint8Array):

const yourUint8Array = Uint8Array.from(atob(base64url.toBase64(req.body.authResponse.rawId)), c => c.charCodeAt(0)).buffer;