April 23, 2020

WebAuthn and Clojure

As I said in my previous post Raytraclj, a raytracer in Clojure I was working on a new article.

In the last week I’ve been playing with WebAuthn, and I wanted to integrate it with Clojure. There’s already a Java library named Webauthn4J which does all the heavy stuff, so it was just a matter of exposing it with a Clojure wrapper.

That’s what I did with the cljwebauthn library.

Webauthn

As stated on webauthn.guide, which I urge you to read if you’re interested in this stuff:

The Web Authentication API (also known as WebAuthn) is a specification written by the W3C and FIDO, with the participation of Google, Mozilla, Microsoft, Yubico, and others. The API allows servers to register and authenticate users using public key cryptography instead of a password.

Here’s a simple flow of how things work:

cljwebauthn

The library API is composed of 4 main functions to deal with the registering and login phase, both having two steps, one for preparing it (generating a challenge) and another one for actually executing the required action.

Backend

Let’s create a simple compojure app and hook the library in it so that we can get started.

I just created a directory structure like this:

$ tree
.
├── deps.edn
├── resources
│   ├── admin.html
│   ├── index.html
│   ├── login.html
│   └── register.html
└── src
    └── app
        └── main.clj

3 directories, 6 files

And edited the deps.edn like this:

{:paths   ["src" "resources"]
 :deps    {org.clojure/clojure            {:mvn/version "1.10.1"}
           buddy                          {:mvn/version "2.0.0"}
           ring                           {:mvn/version "1.8.0"}
           compojure                      {:mvn/version "1.6.1"}
           me.grison/cljwebauthn          {:mvn/version "0.1.2"}
           com.webauthn4j/webauthn4j-core {:mvn/version "0.11.1.RELEASE"}}
 :aliases {:run  {:main-opts  ["-m" "app.main"]}}}

So for this simple app we need clojure, ring + compojure, buddy for authentication and cljwebauth for the WebAuthn stuff.

Let’s now create this simple web application, for this article purpose I won’t have any database, I’ll just store the user in an atom.

Here’s the actual code, first we create our namespace and require everything we need

(ns app.main
  (:gen-class)
  (:require [cljwebauthn.core :as webauthn]
            [buddy.auth.accessrules :refer [restrict IRuleHandlerResponse]]
            [buddy.auth.backends.session :refer [session-backend]]
            [buddy.auth.middleware :refer [wrap-authentication wrap-authorization]]
            [buddy.hashers :as hashers]
            [clojure.java.io :as io]
            [compojure.core :refer [defroutes context GET POST]]
            [ring.adapter.jetty :refer [run-jetty]]
            [ring.middleware.session :refer [wrap-session]]
            [ring.middleware.params :refer [wrap-params]]
            [ring.util.response :refer [response redirect]]
            [clojure.data.json :as json])
  (:import (java.util UUID)))

For this simple app as I said we’re going to have an atom acting as a store, and we’ll need two functions around user management:

  • registering a user given its email and authenticator, so just assoc the map in the atom with these information
  • get the user given its email, so just getting a value from a key in the map
;; This is our application database
;; It will contain the registered users
(def database (atom {}))

;; here we register a user given its email and the webauthn4j authenticator
(defn register-user! [email authenticator]
  (let [user {:id (UUID/randomUUID) :email email :authenticator authenticator}]
    (swap! database assoc email user)))

;; get the user from our fake database using given its email
(defn get-user [email]
  (get @database email))

The browser needs some information to generate the needed credentials so we’ll declare them:

;; This our site properties
(def site
  {:site-id   "localhost",                  ; the site id (for the client)
   :site-name "There's no place like home", ; the site name (for the client)
   :protocol  "http",                       ; the protocol (for webauthn4j)
   :port      8080,                         ; the port (for webauthn4j)
   :host      "localhost"})                 ; the host (for webauthn4j)

As stated at the beginning of the article, cljwebauth, offers 4 functions to deal with the different phases of WebAuthn:

  • prepare-registration
  • register-user
  • prepare-login
  • login

So we’ll implement our ring handlers so that they use these 4 functions.

First let’s see the registration phase:

;; this is the GET /webauthn/login?email=... function
(defn do-prepare-register [req]
  (-> req
      (get-in [:params "email"])           ; get ?email=
      (webauthn/prepare-registration site) ; prepare the registration for this site and email
      clojure.data.json/write-str           
      response))                           ; outputs the result as JSON

;; this is the POST /webauthn/login function
(defn do-register [req]
  (let [payload (-> req :body (json/read-str :key-fn keyword))]         ; get payload
    (if-let [user (webauthn/register-user payload site register-user!)] ; register user 
      (ring.util.response/created "/login" (json/write-str user))       ; 201, and redirect to /login
      (ring.util.response/status 500))))                                ; 500, if something goes wrong

Then, let’s see how to do it for the login phase.

;; this is the GET /webauthn/register?email=... function
(defn do-prepare-login [req]
  (let [email (get-in req [:params "email"])]                     ; get the email
    (if-let [resp (webauthn/prepare-login email                   ; prepare for login (create challenge) 
                (fn [email] (:authenticator (get-user email))))]  ; retrieve the authenticator in our database
      (response (json/write-str resp))                            ; 200 and outputs JSON if everything ok
      (ring.util.response/status
        (json/write-str {:message 
             (str "Cannot prepare login for user: " email)}) 500))))  ; 500 in case something goes wrong

;; this is the POST /webauthn/register function
(defn do-login [{session :session :as req}]
  (let [payload (-> req :body (json/read-str :key-fn keyword))]  ; get payload
    (let [email (cljwebauthn.b64/decode (:user-handle payload))  ; decode the 'user-handle' which is the email 
          user (get-user email)                                  ; retrieve the user from database
          auth (:authenticator user)]                            ; and get its authenticator
      (if-let [log (webauthn/login-user payload site             ; try to login the user by verifying the signature etc.
                 (fn [email] auth))]
        (assoc (redirect "/") :session 
            (assoc session :identity 
               (select-keys user [:id :email])))                 ; add the user to our session so that it can be authenticated later on
        (redirect "/login")))))                                  ; redirect to login if the user could not log-in

The goal for the app is to authenticate a user so that he can access the protected /admin page, so let’s deal with authentication, we need to:

  • know if a user is authenticated
  • wrap the current user in the request so that the handler function can use it if needed
  • have a handler for logging out, which means discarding the session
;; check if a user is authenticated
(defn is-authenticated [{:keys [user]}]
  (not (nil? user)))    ; we just check if we have a 'user' key in our session

;; wrap the user in the request so that the handler can retrieve it if needed
(defn wrap-user [handler]
  (fn [{identity :identity :as req}]
    (handler (assoc req :user (get-user (:email identity))))))

;; log out the user
(defn do-logout [{session :session}]
  (assoc (redirect "/login")               ; redirect to /login
    :session (dissoc session :identity)))  ; but first discard the session

Now we have all the logic, just create the different routes:

(defroutes admin-routes
    (GET "/" [] (fn [_] (slurp (io/resource "admin.html")))))

(defroutes app-routes
    (context "/admin" []   ; only the /admin is restricted to authenticated users
      (restrict admin-routes {:handler is-authenticated}))
    (GET "/" [] (fn [_] (slurp (io/resource "index.html"))))             ; home page
    (GET "/register" [] (fn [_] (slurp (io/resource "register.html"))))  ; register page 
    (GET "/login" [] (fn [_] (slurp (io/resource "login.html"))))        ; login page
    (GET "/logout" [] do-logout)          ; logout page
    
    (context "/webauthn" []                    ; /webauthn
      (GET "/register" [] do-prepare-register) ; prepare registration endpoint
      (POST "/register" [] do-register)        ; registration endpoint
      (GET "/login" [] do-prepare-login)       ; prepare login endpoint
      (POST "/login" [] do-login)))            ; login endpoint

And our application which bootstrap a session backend and then apply multiple middlewares:

(def my-app
  (let [backend (session-backend)]     ; enable session management
    (-> #'app-routes
        (wrap-user)                    ; wrap authenticated user if present
        (wrap-authentication backend)  ; buddy authentication
        (wrap-authorization backend)   ; buddy authorization
        (wrap-session)                 ; wrap session
        (wrap-params))))               ; and request params

At the beginning of our namespace we asked Clojure to generate a Java class so that we can add a main function:

(defn -main []
    (run-jetty my-app {:port 8080 :host "localhost"}))

This is all there is to it. The webauthn stuff takes around 30 lines of code, not that much and it offers great benefits.

Frontend

Now we need a small application, for the purpose of this small article here’s a really simple example using plain old JS with jQuery, no Cljs or reagent for this sample :)

We’ll just talk about the register and login pages.

register.html

For registering the flow is like this:

  • When user click on the register button
    • GET /webauthn/register?email=foo@bar.com
    • Parse JSON
    • Create Public Key credential creation options
    • Generate the credentials
    • POST /webauthn/register
<!doctype html>
<html>
<head>
    ... 
    <script type="application/javascript">
        $ = jQuery;
        $(function () {
            const publicKeyCredentialCreationOptions = (server, email) => ({
                challenge: Uint8Array.from(
                    server.challenge, c => c.charCodeAt(0)),
                rp: {
                    name: server.rp.name,
                    id: server.rp.id,
                },
                user: {
                    id: Uint8Array.from(
                       server.user.id, c => c.charCodeAt(0)),
                    displayName: 'Foobar',
                    name: email,
                },
                pubKeyCredParams: server.cred,
                authenticatorSelection: {
                    authenticatorAttachment: "platform",
                    userVerification: 'discouraged',
                },
                timeout: 60000,
                attestation: "direct"
            });

            $("#register").click(function (e) {
                const email = $("#email").val();
                e.preventDefault();
                $.get("/webauthn/register?email=" + email)
                    .then(resp => $.parseJSON(resp))
                    .then(async resp => {
                        const pubKey = publicKeyCredentialCreationOptions(resp, email);
                        const creds = await navigator.credentials.create({publicKey: pubKey});
                        return {
                            "challenge": resp.challenge, 
                            "attestation": btoa(String.fromCharCode(...new Uint8Array(creds.response.attestationObject))),
                            "client-data": btoa(String.fromCharCode(...new Uint8Array(creds.response.clientDataJSON))),
                        };
                    })
                    .then(payload => {
                        $.ajax({
                            url: "/webauthn/register",
                            type: "POST",
                            data: JSON.stringify(payload),
                            contentType: "application/json",
                            success: function (resp) {
                                alert('You are now registered.');
                            }
                        });
                    });
            })
        })
    </script>
</head>
<body>
...
<form>
    <label for="email">E-mail:</label>
    <input type="text" id="email" name="email" autocomplete="off" />

    <button class="btn btn-primary" id="register">Register</button>
</form>
...
</body>
</html>

login.html

For login the flow is like this:

  • When user click on the login button
    • GET /webauthn/login?email=foo@bar.com
    • Parse JSON
    • Create Public Key credential request options
    • Generate the credentials
    • POST /webauthn/login
<!doctype html>
<html>
<head>
    ... 
    <script type="application/javascript">
        $ = jQuery;
        $(function () {
            const publicKeyCredentialRequestOptions = (server) => ({
              challenge: Uint8Array.from(
                  server.challenge, c => c.charCodeAt(0)),
              allowCredentials: [{
                  id: Uint8Array.from(
                    atob(server.credentials[0].id), 
                    c => c.charCodeAt(0)),
                  type: server.credentials[0].type,
                  transports: ['internal'],
              }],
              timeout: 60000,
            });

            $("#login").click(function (e) {
                const email = $("#email").val();
                e.preventDefault();
                $.get("/webauthn/login?email=" + email)
                    .then(resp => $.parseJSON(resp))
                    .then(async resp => {
                        const pubKey = publicKeyCredentialRequestOptions(resp);
                        console.log(pubKey);
                        const assertion = await navigator.credentials.get({publicKey: pubKey});
                        console.log(assertion);
                        return {
                            "challenge": resp.challenge, 
                            "credential-id": btoa(String.fromCharCode(...new Uint8Array(assertion.rawId))),
                            "user-handle": btoa(email),
                            "authenticator-data": btoa(String.fromCharCode(...new Uint8Array(assertion.response.authenticatorData))),
                            "signature": btoa(String.fromCharCode(...new Uint8Array(assertion.response.signature))),
                            "attestation": btoa(String.fromCharCode(...new Uint8Array(assertion.response.attestationObject))),
                            "client-data": btoa(String.fromCharCode(...new Uint8Array(assertion.response.clientDataJSON))),
                        };
                    })
                    .then(payload => {
                        $.ajax({
                            url: "/webauthn/login",
                            type: "POST",
                            data: JSON.stringify(payload),
                            contentType: "application/json",
                            success: function (resp) {
                                alert('You are now logged-in.');
                            }
                        });
                    });
            })
        })
    </script>
</head>
<body>
...
<form>
    <label for="email">E-mail:</label>
    <input type="text" id="email" name="email" autocomplete="off" />

    <button class="btn btn-primary" id="login">Login</button>
</form>
...
</body>
</html>

Running the app

Just run Clojure with the run alias:

clj -A:run

And locate your Chrome to http://localhost:8080, on my Macbook Pro it asks for my fingerprint to generate the credentials and proceed with the registering and login phase.

You can clone the following repository: agrison/cljwebauthn-sample.

Video

Click below to see a video of the sample application in action:

What’s next

When I’ll have some more time I’ll create modules for compojure and ataraxi so that they can be added really easily for applications using these technologies like standard Clojure backand or Luminus.

You could imagine just one function which creates bootstrap the routes and so on just based on the site properties and the needed functions to save/get the authenticator and check if a user can be registered.

Until next time!

Alexandre Grison - //grison.me - @algrison