Use JSON request body rather than request parameters for Friend authentication in a Clojure Web Application

884 views Asked by At

I am using Friend to build authentication into a Compojure web application.

I have defined a bespoke authentication workflow for Friend:

(defn authentication-workflow []
  (routes
    (GET "/logout" req
      (friend/logout* {:status 200}))
    (POST "/login" {{:keys [username password]} :params}
      (if-let [user-record (authenticate-user username password)]
        (workflows/make-auth user-record {:cemerick.friend/workflow :authorisation-workflow})
        {:status 401}))))

The authentication part is factored out:

(defn authenticate-user [username password]
  (if-let [user-record (get-user-for-username username)]
    (if (creds/bcrypt-verify password (:password user-record))
      (dissoc user-record :password))))

This works, but...

I am using AngularJS and having to post request parameters leads to some ugly Angular code (cribbed elsewhere from a StackOverflow answer):

$http({
    method: 'POST',
    url: '/login',
    headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    transformRequest: function(obj) {
        var str = [];
        for (var p in obj)
            str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
            return str.join("&");
        },
        data: {
            username: username,
            password: password
        }
    });

I would much rather do this much simpler call instead and just post a JSON object via the request body:

$http.post('/login', {username: username, password: password})

I tried to use ":body" in the authentication handler instead of ":params" but the value of :body seemed neither JSON nor Clojure to me so I don't know how I can use it:

{username [email protected], password password}

I already have JSON request/response mapping workflows working correctly for my REST API handlers, and I checked already that the request headers (e.g. ContentType) were correct for JSON.

So can this be done with Compojure/Friend, and if so how?

2

There are 2 answers

0
caprica On BEST ANSWER

Here is some working code and an explanation...

First the Friend workflow, using the request body:

(defn authentication-workflow []
  (routes
    (GET "/logout" req
      (friend/logout* {:status 200}))
    (POST "/login" {body :body}
      (if-let [user-record (authenticate-user body)]
        (workflows/make-auth user-record {:cemerick.friend/workflow :authorisation-workflow})
        {:status 401}))))

Second, the authentication function:

(defn authenticate-user [{username "username" password "password"}]
  (if-let [user-record (get-user-for-username username)]
    (if (creds/bcrypt-verify password (:password user-record))
      (dissoc user-record :password))))

Third, the Compojure application with middlewares declared:

(def app
  (-> (handler/site
        (friend/authenticate app-routes
          {:workflows [(authentication-workflow)]}))
      (params/wrap-keyword-params)
      (json/wrap-json-body)
      (json/wrap-json-response {:pretty true})))

Finally a fragment of AngularJS code to post the credentials (username and password come from an AngularJS model):

$http.post('/login', {username: username, password: password});

So what happens is this...

The Angular javascript code posts JSON to the web application login URL. The "Content-Type" header is automatically set to "application/json" and the request body is automatically encoded as JSON, for example:

{"username":"[email protected]","password":"tumblerrocks"}

On the server, the middleware parses the JSON to a Clojure map and presents it to the handler via the ":body" keyword:

{username [email protected], password tumblerrocks}

The request is then routed to the custom Friend authentication workflow.

Finally the submitted values are extracted from the Clojure map and used to authenticate the user.

0
d.j.sheldrick On

I suspect that your wrappers are applied in the wrong order. Check that ring.middleware.json/wrap-json-body is applied before (outside of) the friend wrapper.

e.g.

(def my-handler (wrap-json-body (cemerick.friend/authenticate ...)))

Otherwise, a quick fix might be to just wrap your whole app in ring.middleware.json/wrap-json-params