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.

(partial creds/bcrypt-credential-fn
  (fn [id] 
    (when-let [{:keys [user password]}
               (find-user-by-user-or-email id)]
             {:username user :password password})))

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.

(defn register-form [ & [errors email username ssh-key]]
  ....)

(defn register [{:keys [email username password confirm ssh-key]}]
  (if-let [errors (validate-profile nil email username password confirm ssh-key)]
    {:status 200 :headers {} :body (register-form errors email username ssh-key)}
    (do (add-user email username password ssh-key)
        (make-auth {:username username}))))

(defn registration [{:keys [uri request-method params]}]
  (when (and (= uri "/register")
             (= request-method :post))
    (register params)))

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.