June 10, 2015

Easy custom auth in Clojure with Friend

Really! It's not as bad as it looks!

I’ve used friend to provide auth in my projects a few times, and considered it many more before resorting to my own hand-rolled business. Most of the reason for this is that is just seems so complicated. Workflows? Credentials? I just want to check a password and stash a user object in the session, end of story. After I did some investigating, though, it turns out that you can usefully deploy friend for even very simple workflows, if you understand how it works.

Friend has its own functions corresponding to those two points: authenticate is a middleware that wraps a Ring handler, while authorize can be deployed just about anywhere in your code to handle that aspect of your program’s auth. We’ll examine authenticate in this post, since the other is pretty simple to use. An illustrative way to learn about how those authentication works in friend is to write a small toy auth system, using none of friend’s built-ins and instead writing our own.

Before that, some glossary

Authentication Map: A hash-map of information about an authenticated user which will be accessible from the Ring request. The :identity key is required, and the :roles key is strongly recommended

Workflow: A function that accepts a request map and returns either an authentication map (for a newly-authenticated user) or nil (if there is no change in the authentication state). It may also return a ring response that will be returned immediately, shortcutting the rest of the request.

Credential Function: A function that accepts a workflow-dependent set of arguments and returns an authentication map.

Speak “friend” and enter

We’ll write a really dumb auth system; instead of a username and password, it will look for a parameter called speak in the incoming request, and authenticate the user if speak=friend. To do that, we’ll need to define a workflow, which we’ll use with friend’s authenticate function to make our authentication middleware.

A workflow is just a function with a specific signature and behavior, which is documented in this hopefully-not-copyrighted diagram:

In short, the workflow accepts a ring request, and can return one of three things:

  • A ring response, which will be returned directly.
  • nil, which will result in other workflows being checked (or a 403 returned if none remain).
  • An authentication map, which means authentication was successful.

The term “authentication map” is ill-defined, but turns out to be any map with the key :identity and with the metadata {:type :cemerick.friend/auth}. Handily, the function cemerick.friend.workflows/make-auth will add this metadata for you.

So let’s just start with a really simple workflow:

(defn fun-workflow [req]
  (let [speak (get-in req [:params :speak])]
    (when (= speak "friend")
      (make-auth {:identity "friend" :roles #{::user}}))))

When a ring request comes in, friend/authenticate will pass it along to this workflow. The workflow will check the value of the “speak” parameter in the params map. If “speak” == “friend”, it calls make-auth on the authentication map, which annotates it with the metadata necessary for friend to recognize it as such.

To hook up your workflow, you’ll need to pass it to the :workflows option in friend/authenticate. Here’s a small but complete example of an app using this auth workflow:

(require  '[compojure.core :refer [defroutes GET POST]]
          '[cemerick.friend :as friend]
          '[cemerick.friend.workflows :refer [make-auth]]
          '[ring.middleware.session :refer [wrap-session]]
          '[ring.middleware.params :refer [wrap-params]]
          '[ring.middleware.keyword-params :refer [wrap-keyword-params]]
          '[ring.adapter.jetty :refer [run-jetty]])

(defroutes app-routes
  (GET "/" [] "Hello everyone <form action=\"logout\" method=\"post\"><button>Submit</button></form>")
  (GET "/authorized" [] (friend/authorize #{::user} "Hello authorized"))
  (friend/logout (POST "/logout" [] "logging out")))

(defn fun-workflow [req]
  (let [speak (get-in req [:params :speak])]
    (when (= speak "friend")
      (make-auth {:identity "friend" :roles #{::user}}))))

(def app
(-> app-routes 
    (friend/authenticate {:workflows [fun-workflow]})

(defn -main []
  (run-jetty #'app {:port 8080}))

If you fire up a server, here’s what you will observe:

  • If you go to /, you’ll see the page regardless of whether you logged in.
  • If you go to /authorized, you’ll be redirected to /login (the default login url, which we never changed and don’t need).
  • If you go to /authorized?speak=friend, you’ll end up at /authorized. You can now freely visit /authorized without adding the query params.
  • If you click the button on / which POSTs to /logout, you’ll be logged out and get the redirect if you try to go to /authorized.

Note that none of this will work without the three ring middlewares I added: wrap-params, wrap-keyword-params, and wrap-session.

What’s the password?

You can definitely write your own workflows this way and have no problems. However, it is recommended for configurability that you split the part of your workflow that checks the auth and retrieves any extra information into a separate credential function like a good lass or laddie.

To use a credential-fn, you just need to provide one to the middleware and make sure the workflow uses it. Here’s how you might break up the above example into a workflow and a credential function:

(defn fun-workflow [req]
  (let [speak (get-in req [:params :speak])
        credential-fn (get-in req [::friend/auth-config :credential-fn])]
    (make-auth (credential-fn speak))))

(defn fun-credential-fn [word]
  (if (= word "friend")
    {:identity word :roles #{::user}}))

;And in the middleware...

    (friend/authenticate {:workflows [fun-workflow]
                          :credential-fn fun-credential-fn})

Make a special note that returning nil is the default. The bare if in the credential-fn returns nil if the word is incorrect, which in turn is returned from the workflow, which means that no change in authentication state occurs.

Now, let’s imagine that instead of a hardcoded magic word, we want auth that will:

  1. Only be processed on the login page on a POST request,
  2. Use a username and password pair, and
  3. Fetch the user object from the database and provide that to every function, instead of just the bare credentials.

Here’s a homemade spin on that, using a magical db namespace whose functions are already written:

(defn do-login [req]
 (let [credential-fn (get-in req [::friend/auth-config :credential-fn])]
   (make-auth (credential-fn (select-keys (:params req) [:username :password])))))

(defn password-workflow [req]
  (when (and (= (:request-method req) :post)
             (= (:uri req) "/login"))
    (do-login req)))

(defn password-credential-fn [{:keys [username password] :as creds}]
  (when-let [user (get-user-by-username username)]
    (when (= (:hashed_password user) (some-strong-hash password))
      {:identity (:id user) :roles #{::user} :user user})))

;And in the middleware...

    (friend/authenticate {:workflows [password-workflow]
                          :credential-fn password-credential-fn})

It’s really not a lot more complicated than the other one, is it?

You can replace password-workflow with the (interactive-form) workflow that comes with friend, and it still works, since “/login” is the default login-uri for friend. I also made sure to match the credential-fn signature.

You can also offload most of the credential-fn to the built-in bcrypt-credential-fn, although this requires that your user map have a :password key that is encrypted with the bcrypt hash function also included with friend. The cemerick.friend.credentials contains the functions hash-bcrypt, bcrypt-verify for those purposes as well.

Other auth schemes

The intent of the credential-fn + workflow model is to be as flexible as possible, and since friend is a middleware, you can even completely abstract workflows like oauth. I mean, it’s still a bit painful to test, but at least the workflow isn’t too bad. Here’s what I came up with writing a friend wrapper around clj-oauth for twitter:

(require '[oauth.client :as oauth])

(def consumer
    (:twitter-consumer-key env)
    (:twitter-consumer-secret env)

(defn twitter-auth-workflow [& {:keys [oauth-callback-uri]}]
  (fn [req]
    (let [credential-fn (get-in req [::friend/auth-config :credential-fn])]


        ;; Login url -- redirect to twitter approval page
        (= (:uri req) (get-in req [::friend/auth-config :login-uri]))
          (let [request-token (oauth/request-token consumer (:uri oauth-callback-uri))
                approval-url (oauth/user-approval-uri consumer (:oauth_token request-token))
                resp (resp/redirect approval-url)]
            ;; Stash the request-token in the session for later
            (update-in resp [:session] assoc :oauth-request-token request-token))

        ; Oauth callback
        (= (:path oauth-callback-uri) (:uri req))
          (when-let [request-token (get-in req [:session :oauth-request-token])]
            (when-let [access-token (oauth/access-token
                                      (get-in req [:params :oauth_verifier]))]
              (make-auth (credential-fn access-token))))))))

(def oauth-callback-uri {:url "http://localhost/oauth/callback"
                         :path "/oauth/callback"})

  {:workflows [(twitter-auth-workflow :oauth-callback-uri oauth-callback-uri)]
   :credential-fn #(hash-map :identity % :roles #{::user})})

If you’ve ever tried to write any oauth-based auth system, you’d think that’s not too bad! Since the authentication middleware receives the whole request, it can do the oauth redirect on the login url and handle the callback as well, without getting the way of everything else. After all that, friend handles authorization for you, without having to go through it again!

That’s why friend is worth using: authentication is as hard a problem as it ever was, but at least friend solves authorization for you with almost no effort, and does a better job than if you tried to do it yourself.