Basic auth in Clojure

This week I wanted to implement a simple authentication mechanism for a small website. I didn't want to add any complicated login mechanism, and I wanted to customize the site based on who logged in. Here's how I did it so I using a basic auth and ring middleware.

In the app I'm building every user has an API key, and this is tied to a unique user id. The idea is that users will be prompted with a login window, input their API key as username, and then the service will know all the relevant user information (which the site gets from another service). This small guide assumes you are using Leiningen and Ring.

The only dependency we need is ring-basic-authentication, and the only function we'll use from it is wrap-basic-authentication. Find the latest version at Clojars and include it in your project.clj and relevant namespace form.

Here's what the handler looks like, which you can pass into something like run-jetty to run your server.

(def app
  (-> app-routes
        wrap-add-params
        (wrap-basic-authentication authenticated?)
        handler/site))

app-routes is your normal Compojure defroute routes, and handler/site is a bunch of convenience middleware from Compojure to deal properly with forms etc. (see Compojure documentation to find out what it includes). If you don't really understand what middleware is or does, the best explanation I'm aware of can be found at the official Ring Wiki under their Concepts page (https://github.com/ring-clojure/ring/wiki/Concepts#middleware).

Here's the gist of the wrap-basic-authentication function documentation: Wrap response with a basic authentication challenge. [...]The authenticate function is called with two parameters, the userid and password, and should return a value when the login is valid. This value is added to the request structure with the :basic-authentication key.

This is part of the HTTP specification and it's called Basic Authentication. In your browser it shows up as a popup prompting you to input a user and password. The etymology of the words in "Basic Authentication" provides some insight into what it is. It authenticates because it establishes the credibility of the user, it verifies it. It's basic in a sense because it's the simplest solution, and the solution is rudimentary. It doesn't provide confidentiality, that is, full trust that the user is who they say they are. This is why it's recommended to only use BA with HTTPS . Without HTTPS it it's easy for someone to pretend to be someone they are not - all they have to do is eavesdrop on your HTTP requests with a tool like Firesheep and they can see the token you are sending in plaintext.

Here's what the authenticated? function looks like.

(defn authenticated? [u _]
  (let [m {"foo" "bar"
               "baz" "quux"}]
    (when (contains? m u) (get m u))))

This function simply returns whatever associated value we have to our API key. If it doesn't exists it returns nil. The map m could be anything - for example it could be a plain text file with a mapping from API keys to UUIDs, or it could be hardcoded in the code like in the above listing. The next step is to take the associated value and put it somewhere more useful, like for example the in the parameters map for easier access when dealing with user input.

(defn wrap-add-params [handler]
  (fn [req]
    (let [req (update-in req
                                [:params]
                                #(assoc % :user (req :basic-authentication)))]
      (handler req))))

This updates the Ring request map so we can access it in the params map, which is what we will use for user submitted form. There we can easily use the added user information to make the end user fill in less information. While not strictly necessary, it's a nice hack that makes things easier for us, and shows how we can modify requests as we wish.

The aim of this article is to write something that is useful for at least one person. I believe it would've been useful for me a couple of days ago, so I hope this is useful for someone else!

That's all there is to it. Three functions and one dependency is all it takes to add basic authentication with customized content to your Clojure web app.

PS I'm sure this is unsafe in many ways. If you know how you would hack it, let me know in the comments! DS