Clojure Bites - Ring basic auth

by FPSD — 2023-07-06


Overview

I am on vacation so I have decided to take it easy and work on small things on Unrefined; one item in the backlog seemed quite approachable: adding an admin panel to the project, which requires, among other things, a way to authenticate users to access those restricted sections.

Here is a quick intro to how to add Basic Auth support to a Ring web application, using ring-basic-authentication middleware, and, as a bonus, an intro to clojure.core.cache library.

Project setup

To show how the middleware works we can create a simple web application with just one endpoint which later we can protect with basic auth. Start a new project or use your favorite playground and add the following dependencies:

Feel free to use your preferred project and dependency management tool, here I show how to do that with tools-deps, it is easy to translate it to lein or boot:

{:paths ["src" "resources"]
 :deps {org.clojure/clojure {:mvn/version "1.11.1"}
        ring/ring-core {:mvn/version "1.10.0"}
        ring/ring-jetty-adapter {:mvn/version "1.10.0"}
        ring-basic-authentication/ring-basic-authentication {:mvn/version "1.2.0"}}
 :aliases
 {:dev/repl {:main-opts ["-m" "nrepl.cmdline" "–middleware" "[cider.nrepl/cider-middleware]"]
             :extra-paths ["dev"]
             :extra-deps {cider/cider-nrepl {:mvn/version "0.31.0"}
                          djblue/portal {:mvn/version "0.35.1"}
                          com.github.jpmonettas/clojure {:mvn/version "1.11.1"}
                          com.github.jpmonettas/flow-storm-dbg {:mvn/version "3.3.315"}}
             :jvm-opts ["-Dclojure.storm.instrumentEnable=true"]}
  }}

The extra alias :dev/repl is my usual goto setup for development, it is not required to run the examples but it can be generally handy; feel free to ignore it if you have your own way.

Now we can create a new src/app.clj (or whatever ns you prefer) with the following content:

(ns app
  "A namespace used to show how the basic-authentication middleware works"
  (:require [ring.adapter.jetty :as jetty]))

(defn handler
  "Our basic ring handler function, it takes a request map (unused) and
   return a response map"
  [_request]
  {:status 200
   :headers {"Content-Type" "text/html"}
   :body "Hello World"})

;; We want to hold the server instance in order to close it once done.
;; There are better ways to do it, using life cycle/state management libraries
;; like mount, integrant, component just to name few, but lets keep it simple
(def server-instance (atom nil))

(defn start-server
  "Starts a web server holding its instance in the server-instance atom"
  []
  (reset! server-instance
          (jetty/run-jetty handler {:port 3000
                                    :join? false})))

(defn stop-server
  "If there is a server running, stop it and reset the server-instance atom"
  []
  (swap! server-instance
         (fn [inst]
           (when inst
             (.stop inst)))))

(comment
  (start-server)
  (stop-server)
  ,)

If we start a REPL and evaluate the buffer (file) we can start the server by evaluating the fist form in the comment form, and pointing a browser to http://localhost:3000 we should be able to see something like this:

basic-handler-output

Protecting the route

The library ring-basic-authentication offers the ring middleware wrap-basic-authentication that will call a function accepting a username and a password and returns a truty value on success (authenticated) or a falsy value otherwise. On success the next middleware (or handler) will be called, otherwise it will return a 401 response.

The first thing we can do is to write a function that will authenticate (or not) a request based on provided username and password, here is the simplest implementation we can write:

(defn authenticated?
  [username password]
  (and (= "secret-username" username)
       (= "secret-password" password)))

Now it is time setup ring to use the middleware to authenticate requests. Lets re-write the start-server function to do that:

(defn gen-app
  []
  (wrap-basic-authentication handler authenticated?))

(defn start-server
  "Starts a web server holding its instance in the server-instance atom"
  []
  (reset! server-instance
          (jetty/run-jetty (gen-app) {:port 3000
                                      :join? false})))

The utility function gen-app returns a new handler using the wrap-basic-authentication middleware to wrap our request handler; start server calls the gen-app function when starting the server, to get the application handler function.

We can see it in action in the following screenshot

basic-auth-form

Now for each request this is the flow:

In ring world this is the usual flow; in general we can expect to have chain of middlewares before the call to the request can even start, common middlewares can be used for:

There are quite a lot of already existing and useful middlewares that can be used off the shelf.

Even if this approach can be good enough for some small and private services it can become unusable or hard to maintain pretty easily if the service grows or must be exposed to the public internet; few issues that can come to mind:

Lets make it flexible

First thing we can do to improve this example is to get rid of those ugly hardcoded credentials.

One way could be to load the username and password from environment variables; as one extra step would be to on the safe side we can avoid having defaults so that if we forget to set the environment variables no one can access the restricted resources. Here is one possible way to authenticated? for our new requirements:

(let [secret-username (:basic-auth-username env "")
      secret-password (:basic-auth-password env "")]
  (defn authenticated?
    [username password]
    (and (seq secret-username) ;; making sure this two are set to something
         (seq secret-password) ;; TIL that (seq "") is equivalent to (not (empty? "")), thanks clj-kondo!
         (= secret-username username)
         (= secret-password password))))

Wait wait wait, where is this env coming from?

Even if is possible to get the value of environment variables using Java interop via System/getenv I have decided to introduce the library environ because it offers a Clojure only interface and provides more goodies like reading environment variables from env files or JVM properties and has a nicer interface (IMHO).

To be able to use this library all we need to do is to add it to our deps.edn (or…you know) and refer the env symbol in our namespace. For more details please refer to the library docs, all we need to know now is that it behaves like a normal map, at least for reading.

Oh, one more thing, keywords are translated to "UPPERSNAKECASE" when looking them up, in our case :basic-auth-username will lookup BASIC_AUTH_USERNAME environmental variable.

Even more flexible

Now we want to give access to the protected resource to more people and usually sharing the same credentials is not a good practice; what happens if we want to invalidate the access for one of the users? We should create new credentials and share it only with the ones whom should have access to the resource. It is not really practical, is it? So at this point we may want to have user specific credentials but handling them with env vars can be too complicated or unpractical.

An option is to store credentials in a file that we can read at the start of the application, in case someone leaves our organization we can update our credentials file and restart the app. For convenience we can read the credentials file path from an environment variable, we already know how to do it right? Right.

(defn get-credentials
  "Return a username -> password map reading it form the specified file, or an
   empty map it reading fails"
  [credentials-path]
  (try
    (with-open [r (io/reader credentials-path)]
      (edn/read (java.io.PushbackReader. r)))
    (catch Throwable  _ {})))

(defn authenticated?
  [username password]
  (let [credentials (get-credentials (:basic-auth-credentials env "credentials.edn"))]
    (= (get credentials username) password)))

Now authenticated? will read the credentials from an edn file whose path is taken from the BASIC_AUTH_CREDENTIALS environment variable (defaulting to some well known path). If the credentials will ever change we must restart the application to see the new values, not the end of the world but we can do a bit better.

The idea is to cache the credentials with a TTL, so when it expires we can read the file again with possibly new values. Implementing a cache with TTL is a nice exercise but we can leverage the clojure.core.cache library that implements this functionality for us. I am not going to write about this library in detail, the documentation does a great job anyway, so I'll focus on the bare minimum to get things done.

Assuming that we have added the library to our dependencies and required it in our namespace, we can change autheticated? to make use of it:

;; create a TTL cache with an empty map and a ttl of 60 seconds
(def credentials-cache (cache/ttl-cache-factory {} :ttl 60000))

(defn authenticated?
  [username password]
  (let [credentials (cache/lookup-or-miss
                      credentials-cache
                      :credentials ;; we try to lookup the key :credentials in the cache
                      (fn [_]      ;; if the key is missing or expired we load the credentials file
                        (get-credentials (:basic-auth-credentials env "credentials.edn"))))]
    (= (get credentials username) password)))

This is a sorts of an hack of the TTL cache; usually you want to cache with TTL different keys, and load the value of those keys (from DB, external APIs, whatever) when the TTL expires. In our case we just want to load the whole file so we are using the key :credentials to hold its content. The clojure.core.cache implements quite a lot of caching strategies, I encourage everyone to give it a try!

Conclusions

If dealing with a simple application, using basic auth can be good enough and we have explored different ways to achieve that; clearly, as the application grows, other authentication mechanisms like a full suited identity/role management solution may be a better fit. Currently I am exploring Keycloak, hoping to come up with a library for Clojure.

Sources for this little project can be found in my shiny new playground repo, specifically in the ring_basic_auth.clj file

Alternatives

Every web server provides a way to protect one (or more) endpoint at the server configuration level, sometimes it can be even convenient to leverage that functionality; the only downside I see in this approach is that it couples the application to the web server serving it. If the endpoint structure is sufficiently complex it is easy to forget something in case of a migration to a different web server.

Another alternative is to use the same authentication/authorization mechanism used for normal users and play with roles and permissions to enable/disable access to specific resources. In the longer term this is the approach I would take given that it reduces special cases and makes role management more consistent.

Discuss