Reputation: 4146
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?
Upvotes: 3
Views: 881
Reputation: 4146
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.
Upvotes: 3
Reputation: 1591
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
Upvotes: 1