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]})
(wrap-keyword-params)
(wrap-params)
(wrap-session)
))
(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:
- Only be processed on the login page on a POST request,
- Use a username and password pair, and
- 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
(oauth/make-consumer
(:twitter-consumer-key env)
(:twitter-consumer-secret env)
"https://api.twitter.com/oauth/request_token"
"https://api.twitter.com/oauth/access_token"
"https://api.twitter.com/oauth/authorize"
:hmac-sha1))
(defn twitter-auth-workflow [& {:keys [oauth-callback-uri]}]
(fn [req]
(let [credential-fn (get-in req [::friend/auth-config :credential-fn])]
(cond
;; 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
consumer
request-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"})
(friend/authenticate
{: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.