Using friend in clojars
In the ruby web world there is a great set of libraries for authentication in warden and devise. Earlier this year, Chas Emerick released friend, which is a similar library for clojure. When I first started contributing to clojars, it was using custom authentication and authorization functions. After friend was released I was able to remove most of this code.
Use bcrypt
Around this time Phil Hagelberg had changed clojars to use
bcrypt.
This made it easy to use friend’s bcrypt-credentials-fn
to do the
password checking. It just required querying the database with
find-user-by-user-or-email
, and to return the proper map for
bcrypt-credentials-fn
. Note: :roles
are not used in clojars; why is
explained later.
Interactive Form Workflow
Friend comes with a interactive-form
workflow. It works by
listening for a :post
on /login
(by default) and using the
credential function to attempt the login. In the case of success it
will forward to the url with unauthorized access. In the case of
failure it will redirect back to /login?login_failed=..&username=..
.
This was different then the previous clojars authentication style,
which would render the login form directly on an unsuccessful login.
Rather then building a new workflow, the/login
handler was adapted
to check for those params and render a login failed message.
The clojars logout functionality also had to be replaced with friend’s
logout
middleware, but that was easy to use.
Registration Workflow
Friend does not come with a workflow for registering. However, it was easy enough to change the clojars registration pages into one.
A :get
to /register
can just render the register-form
. By
including registration
as a workflow, it will listen for a :post
to /register
and either return a ring response with the form to try
again, or add the user and return a friend “authentication map”. Yes,
this is a different pattern then the interactive workflow above.
Authentication not Authorization
While exploring friend, a downside was noted with the authorization.
When a user is authenticated, the :roles
are added into the session. If
the user gets a new role it will not take effect without a
login/logout. Using derive
to produce a in memory role hierarchy
can work with this, as suggested in the friend readme.
This seemed like a bad idea for clojars. The direct mapping of groups
to roles would require trying to keep the roles in sync with the
database, and reinitialized on a restart. Instead clojars uses its own
mechanism for authorization wrapping (friend/throw-unauthorized friend/*identity*)
.
After discussion with Chas an issue has been filed at https://github.com/cemerick/friend/issues/21. It sounds like there will be work in this area in the future.
Benefits
Moving to friend allowed removal of several pieces of code. The
ability to deploy to /repo
over https was developed shortly
afterwards. It was nice to wrap a friend middleware with the
http-basic
workflow, have it use the same credentials function, and
just work.