Go-ing to JS?

Tl;dr

Demo (on GitHub pages, server in browser, storage in LocalStorage): https://pulsejet.github.io/go-webauthn-js/

Repository: https://github.com/pulsejet/go-webauthn-js

NPM package: https://www.npmjs.com/package/go-webauthn-js

Express server: https://github.com/pulsejet/go-webauthn-js-server

WebAuthn

The Web Authentication API can be notoriously hard to implement on the server side, owing to the various devices, key types and what not. A while back, I had the idea of adding a hardware based second factor in a proprietary SSO system written completely in node, but I was greeted by lack of popular or maintained server side libraries for implementing the protocol. While some attempts have been made, including webauthn and @webauthn/server , they are either incomplete or not ready for production. Not wanting to implement dozens of complex protocols on my own, I just gave up.

Revisit

Yesterday, I happened to (fortunately) revisit this idea, propelled by a nice demo that I saw online. While the libraries for node still seemed to be at the same stage, I found a nice and complete Go library by duo-labs on GitHub, that did everything that I wanted. However, there was no way I could run a separate server in Go with all the database connections and other shenanigans just for this. So I tried to find a better solution — some way to call go functions from node.

Enter GopherJS

GopherJS offers a very intriguing proposal — compiling Go programs to JavaScript. The purpose of the project is primarily to run Go code in the browser, but it works equally well with node. I had to install Go 1.12 to get it to work, but everything compiled as I would expect it to, and I could write a simple interface that would let me communicate with the library with stringified JSON.

However, when I tried to register a WebAuthn device, I started getting mysterious errors that made no sense. Digging down deep into the stack traces, it turned out that the library used for CBOR decoding (ugorji/go/codec) was guessing incorrect types (as far as I think!), and so CBOR would always fail to decode. Nothing worked beyond the initial call. My initial guess was that this was because the webauthn library needed Go 1.13, but GopherJS was stuck with 1.12.

Enter WebAssembly

So I decided to give a shot to WASM, which Go has native support for. Compiling with Go 1.13 didn’t work, but I could find related issues that had been fixed with the recently released 1.14, which let me compile the library to WASM.

Everything worked! I could register my laptop with a TPM module as a device and login again right from the browser! Quickly, I tried it out in node and it worked there as well, letting me build an express app with webauthn support.

Again, it did have some issues. The WASM file was huge, at ~14MB (gzipped~4MB), and was very slow to start. It also seemed like a hack, keeping the Go WASM “app” running in background while calling its functions (this is how it works, apparently).

Re-enter GopherJS

Next I tried using a build of GopherJS for Go 1.13/1.14. It still got the same error as before, indicating something else was amiss here. Not wanting to go down the same road again, I simply switched the library to a much lighter one (fxamacker/cbor), and everything started working!

I now had a JS bundle that started almost immediately, weighed around 5 MB after minification and ~2MB after gzip.

Trimming it down

While having a lot of safeguards is a good thing, I nonetheless decided to try trimming down the bundle a bit. Taking a look at the minified GopherJS bundle, the biggest library in use seemed to be net/http . Not very useful either … hmm. Tracking down its usage, I had to remove a dependency and break some stuff, but with some effort, I could finally get rid of it from the bundle. The result — 2.4MB after minification with closure compiler and ~600KB gzipped :D

Packaging it

Writing a tiny JS wrapper around it, I could publish a small NPM package that lets you create webauthn servers in node (go-webauthn-js). Further, a small example server in express that uses the same can be found here. Note that this is not production ready by any means.

But the best part (and the most irrelevant), is that it can run completely in the browser. Try it out!

Go JavaScript!

(pun intended?)