WebAuthn and Clojure

栏目: IT技术 · 发布时间: 4年前

内容简介:As I said in my previous postIn 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
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:

WebAuthn and Clojure

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 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 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:

WebAuthn and Clojure

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!


以上所述就是小编给大家介绍的《WebAuthn and Clojure》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

VISUAL BASIC 6.0 WINDOWS API讲座

VISUAL BASIC 6.0 WINDOWS API讲座

王国荣 / 人民邮电出版社 / 1999-06-01 / 76.00元

本书全面介绍了在Visual Basic 6.0中如何调用Windows API的技术,特别是结合读者在应用中经常遇到的具体问题编写了许多应用范例,书中还给出了API函数的速查表。本书主要内容包括: Windows API的基本概念和调用方法,资源文件的使用,Windows的消息系统及其应用,API在绘图中的应用,多媒体文件的播放,特殊命令按钮的制作等。 本书适用于已熟悉Visual Basic的一起来看看 《VISUAL BASIC 6.0 WINDOWS API讲座》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

在线进制转换器
在线进制转换器

各进制数互转换器

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具