JSON authentication with cemerick/friend?

381 views
Skip to first unread message

Ivan Schuetz

unread,
Apr 23, 2014, 6:28:45 PM4/23/14
to clo...@googlegroups.com
Hi,

I'm trying to get a simple use case running - send a login request to /login and get success or fail response, preferably in JSON format.



(def users {"root" {:username "root"
                    :password (creds/hash-bcrypt "admin_password")
                    :roles #{::admin}}
            "jane" {:username "jane"
                    :password (creds/hash-bcrypt "test")
                    :roles #{::user}}})


(defroutes app-routes

  (GET "/test" [] {:body {:my-map "helo"}})
  
  (route/resources "/")
  
  (route/not-found "Not found"))

(def app
  (->
      (handler/api app-routes)
      (middleware/wrap-json-body)
      (middleware/wrap-json-response)

       (friend/authenticate {:credential-fn (partial creds/bcrypt-credential-fn users)
                            :workflows [
                                        (workflows/interactive-form)]})
      )
  )



I'm testing with curl:

curl -v --data "username=jane&password=test" http://localhost:3000/login

Or:


And I get:

* About to connect() to localhost port 3000 (#0)
*   Trying ::1...
* connected
* Connected to localhost (::1) port 3000 (#0)
> POST /login?username=jane&password=test HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8y zlib/1.2.5
> Host: localhost:3000
> Accept: */*
< HTTP/1.1 302 Found
< Date: Wed, 23 Apr 2014 22:25:15 GMT
< Content-Length: 0
< Server: Jetty(7.6.8.v20121106)
* Connection #0 to host localhost left intact
* Closing connection #0


This looks like authentication failed, but the data is correct. I reviewed the curl request, and this seems to be the correct way to send a POST. But &username= gives me the impression it's not being parsed correctly.

Also, how can I get a JSON response instead of only a header?

Thanks.

P.S. Maybe it would be positive if this library has an own Google Group.

Sam Ritchie

unread,
Apr 23, 2014, 8:11:41 PM4/23/14
to clo...@googlegroups.com
I wrote a post that tries to explain all the crazy cases you need to handle to dispatch Friend responses based on content type:

http://sritchie.github.io/2014/01/17/api-authentication-with-liberator-and-friend/

Liberator can be helpful for this.

April 23, 2014 3:28 PM
--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to
clojure+u...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to the Google Groups "Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clojure+u...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

--
Sam Ritchie (@sritchie)

Erik Bakstad

unread,
Apr 24, 2014, 2:51:29 AM4/24/14
to clo...@googlegroups.com
Here is an example from our ajax-login form. After reading Sam's excellent writeup it should be understandable.

Ivan Schuetz

unread,
Apr 25, 2014, 12:47:11 PM4/25/14
to clo...@googlegroups.com
Hmm... Ok I read http://sritchie.github.io/2014/01/17/api-authentication-with-liberator-and-friend/

for now I would like to get it working without using Liberator. Probably I'll add it later. The part explaining the authentication explains a lot but still couldn't figure out how to implement json login.


I tried Erik's snippet, but it's not complete and I can't figure out how to use it.

ajax-login is a workflow... right? I tried this:


(def app
  (->
      (handler/api app-routes)
      (middleware/wrap-json-body)
      (middleware/wrap-json-response)

       (friend/authenticate {:credential-fn (partial creds/bcrypt-credential-fn users)
                            :login-url "/login"
                            :workflows [
                                        ;(workflows/http-basic :realm "/")
                                        (workflows/interactive-form)
                                        (ajax-login)
                                        ]})

      )
  )


Then I test with curl:

curl -i -H "content-type: application/json" -X POST -d "username=jane&password=test" http://localhost:3000/login 

And I get again:

HTTP/1.1 302 Found
Date: Fri, 25 Apr 2014 16:47:15 GMT
Content-Length: 0
Server: Jetty(7.6.8.v20121106)


So I debugged the parameters passed to the ajax-login...

      (println (str "headers: " headers))
      (println (str "request-method: " request-method))
      (println (str "params: " params))
      

And I see:

headers: {"user-agent" "curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8y zlib/1.2.5", "content-type" "application/json", "content-length" "29", "accept" "*/*", "host" "localhost:3000"}
request-method: :post
params: 


So apparently the post data is not being passed...?

I also tried (randomly) commenting this out
(workflows/interactive-form)

As expected no success, get a different error.

BTW, I'm implementing this for an iOS app not ajax, but it shouldn't make any difference. Anyway for now I just want it to work with curl.


Thanks for your help.

Ivan Schuetz

unread,
Apr 25, 2014, 12:53:42 PM4/25/14
to clo...@googlegroups.com
Also I couldn't find which library you're using for "get-headers" function... or is it a self made one?

I found a get-headers in ring-servlet...

Added this dependency [ring/ring-servlet "1.3.0-beta1"] didn't work

And also in a netty adapter library
 [ring-netty-adapter "0.0.3"]

Also didn't work... for now I commented this line out, since it's not necessary to get my curl test working. But it would be nice if you provide the dependencies.



Am Donnerstag, 24. April 2014 08:51:29 UTC+2 schrieb Erik Bakstad:

Ivan Schuetz

unread,
May 5, 2014, 5:49:29 PM5/5/14
to clo...@googlegroups.com
Concerning the workflow with the ajax-login... for some reason the middleware to set params with json body of my POST isn't working.

As a workaround I added this to ajax-login, to parse the params:

(checore/parse-string (slurp (:body request)) true)      



I had also to remove the interactive-form workflow I had... now my middleware looks like this:


       (friend/authenticate {:credential-fn (partial creds/bcrypt-credential-fn users)
                            :login-url "/login"
                            :workflows [
                                        (ajax-login)
                                        ]})



I do have the middlewares supposedly responsible for setting :params ... not idea why this is not being done. This is the complete block:

(def app
  (->
      (handler/api app-routes)

      (format/wrap-restful-format)
            
      (middleware/wrap-json-body)
      (middleware/wrap-json-params)
      (middleware/wrap-json-response)

       (friend/authenticate {:credential-fn (partial creds/bcrypt-credential-fn users)
                            :login-url "/login"
                            :workflows [
                                        (ajax-login)
                                        ]})

      )
  )

But well, it works for now.

Sam Ritchie

unread,
May 5, 2014, 7:18:54 PM5/5/14
to clo...@googlegroups.com
    (middleware/wrap-json-params) slurps the body up completely - this is a mutation, so you won't be able to access the body again.

May 5, 2014 3:49 PM
--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to
clojure+u...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to the Google Groups "Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clojure+u...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
April 25, 2014 10:53 AM
Also I couldn't find which library you're using for "get-headers" function... or is it a self made one?

I found a get-headers in ring-servlet...

Added this dependency [ring/ring-servlet "1.3.0-beta1"] didn't work

And also in a netty adapter library
 [ring-netty-adapter "0.0.3"]

Also didn't work... for now I commented this line out, since it's not necessary to get my curl test working. But it would be nice if you provide the dependencies.



Am Donnerstag, 24. April 2014 08:51:29 UTC+2 schrieb Erik Bakstad:
--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to
clojure+u...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to the Google Groups "Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clojure+u...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
April 24, 2014 12:51 AM
Here is an example from our ajax-login form. After reading Sam's excellent writeup it should be understandable.

https://gist.github.com/ebaxt/11244031

kl. 00:28:45 UTC+2 torsdag 24. april 2014 skrev Ivan Schuetz følgende:
--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to
clojure+u...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to the Google Groups "Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clojure+u...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
April 23, 2014 4:28 PM
--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to
clojure+u...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to the Google Groups "Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clojure+u...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Ivan Schuetz

unread,
May 5, 2014, 8:17:52 PM5/5/14
to clo...@googlegroups.com
The snippet provided by Eric doesn't use the body... it returns a function that accepts params as argument, but they were not being passed... body was set instead, that's why I added slurp.

So maybe there's something wrong with the  (middleware/wrap-json-params), because it's evidently not having effect.

Ivan Schuetz

unread,
May 5, 2014, 8:31:30 PM5/5/14
to clo...@googlegroups.com
Sorry. I mean accepts the request map as an argument. This map doesn't contain the parameters.
Message has been deleted

Erik Bakstad

unread,
May 6, 2014, 2:03:47 AM5/6/14
to clo...@googlegroups.com
We're using a custom jsonparam-extractor middleware that basically does this:

(update-in req [:params] assoc :json-params (read-json (slurp (:body req)) true))

I should probably have clarified this, sorry.

Also, we're not using interactive-form so I haven't tested if the two workflows work together. It shouldn't be a problem to make them work together though.

Ivan Schuetz

unread,
May 6, 2014, 6:04:10 AM5/6/14
to clo...@googlegroups.com
Thanks. I just found out that my middlewares work, I just had to reorder them like this:

(def app
  (->
      (handler/api app-routes)
         (friend/authenticate {:credential-fn (partial creds/bcrypt-credential-fn users)
                            :login-url "/login"
                            :workflows [
                                        (ajax-login)
                                        ]})

      (format/wrap-restful-format)
            
      (middleware/wrap-json-body)
      (middleware/wrap-json-params)
      (middleware/wrap-json-response)

      )
  )

Now I don't need slurp... still had to change this line in ajax-login, since the format using these middlewares is a bit different:

              (if-let [{:keys [username password] :as creds} (get-in request [:params :json-params])]

with:

              (if-let [{:keys [username password] :as creds} (clojure.walk/keywordize-keys(:json-params request))]

Erik Bakstad

unread,
May 6, 2014, 6:56:33 AM5/6/14
to clo...@googlegroups.com
You can also pass inn options to wrap-json-body so you don't have to use keywordize-keys:

(def app
  (wrap-json-body handler {:keywords? true :bigdecimals? true}))

Ivan Schuetz

unread,
May 13, 2014, 4:05:08 PM5/13/14
to clo...@googlegroups.com
Okay. One question, what is the point to implement this as middleware? It's just a request to /login, which checks the credentials against a database and returns the status as JSON... in my case the HTTP status code will not change... (200 for success/failure). I thought it was maybe the case to modify the HTTP headers, e.g. to add a session id, but it seems this is also possible with normal handlers.

I would see sense in using a middleware to protect resources that need authentication. But don't see it right now for /login service.
Reply all
Reply to author
Forward
0 new messages