I have written a simple contrived auth function in Clojure and it doesn't feel very idiomatic to me at all. Is there a better way of writing this:
(defn auth [username password]
(let [user-record (credential-fn username)]
(if (and user-record (verify-pw password))
(let [user-record (dissoc user-record :password)]
{:status 200 :body user-record})
{:status 401})))
I thought it might be possible to get rid of the if by using if-let, but the if is doing a boolean check and I need to bind the user-record? Stuck!
NOTE: the dissoc is removing the password from user-record so it's not returned in the body.
I think the biggest problem with your function is that it tries to handle three things at once:
Checking if username and password are valid for a user (credential-fn and verify-pw)
Cleaning up the record data (dissoc user-record password)
Building a Ring response map ({:status 401} vs. {:status 200 :body user-record})
I would consider splitting your code into two separate functions:
(defn authenticate
[username password]
(and (verify-pw password)
(dissoc (credential-fn username) :password)))
(defn login
[username password]
(if-let [user-record (authenticate username password)]
{:status 200 :body user-record}
{:status 401}))
Related
after doing web development for ages and discovering Clojure a year ago, I want to combine these two things.
After starting with Compojure, I try to implement authentication by using a middleware which responds with a 403 code, telling the user to authenticate.
This is my code:
(defn authenticated? [req]
(not (nil? (get-in req [:session :usr]))))
(defn helloworld [req]
(html5
[:head
[:title "Hello"]]
[:body
[:p "lorem ipsum"]]))
(defn backend [req]
(html5
[:head
[:title "Backend"]]
[:body
[:p "authenticated"]]))
(defroutes all-routes
(GET "/" [] helloworld)
(context "/backend" []
(GET "/" [] backend)))
(defn wrap-auth [handler]
(fn [req]
(if (authenticated? req)
(handler req)
(-> (response "Nope")
(status 403)))))
(def app
(-> (handler/site all-routes)
(wrap-auth)
(wrap-defaults site-defaults)))
Here comes the funny part: If I run the code as shown above, Firefox breaks with the error message "File not found". Opening the debug toolbar, I see a 403 response and the content "Tm9wZQ==" which is base 64 decoded the "Nope" from my auth middleware function. When I put wrap-auth after wrap-defaults everything works fine.
I want to understand what's going on there. Can you help me?
It's really difficult to say what's going on under the hood. The wrap-defaults middleware brings lots of stuff, maybe 10 or more wrappers at once. You'd better to examine its source code and choose exactly what you need.
I may guess that, for some reason, the Ring server considers your response being a file, so that's why it encodes it into base64. Try to return a plain map with proper headers as follows:
{:status 403
:body "<h1>No access</h1>"
:headers {"Content-Type" "text/html"}}
I have an endpoint called /account which provides user info(returns html).
When unauthorised user tries to access this endpoint I need to be able to redirect to login page but in Liberator I found post-redirect so far and it is just for post methods.
I need to redirect get methods as well, how can I achieve this?
I found a workaround following code does the trick:
(defn account
[]
(resource :allowed-methods [:get]
:available-media-types ["text/html"]
:exists? (fn [_] false)
:existed? (fn [_] true)
:moved-temporarily? (fn [ctx] {:location "/redirected-path-or-url"})
:handle-ok (fn [ctx]
[:html ...])
:handle-exception (fn [_]
"Something went wrong")))
Or you can check :authorized? and return login html from :handle-unauthorized but I doubt it about it's a good practice or not.
I'm trying to write a custom middleware that checks if user is authenticated by checking existence of :user key in request.
(defn wrap-authenticated [handler]
(fn [{user :user :as req}]
(if (nil? user)
(do
(println "unauthorized")
{:status 401 :body "Unauthorized." :headers {:content-type "text/text"}})
(handler req))))
(def app
(wrap-authenticated (wrap-defaults app-routes (assoc site-defaults :security false))))
But when I try to return response hashmap with 401 status, I get the following exception:
WARN:oejs.AbstractHttpConnection:/main
java.lang.ClassCastException: clojure.lang.Keyword cannot be cast to java.lang.String
Perhaps I don't understand the logic that needed to be implemented inside Compojure middleware.
How do I write my middleware that breaks the middleware chain and just returns custom response or redirects to handler?
I believe in your case the mistake is in the :headers map, since the keys are expected to be strings and you're using :content-type, which is a keyword. Try this instead:
{"Content-Type" "text/html"}
I'm struggling with understanding how to properly use sessions in Compojure/Ring.
Some of the examples I have come across:
https://github.com/brentonashworth/sandbar-examples/blob/master/sessions/src/sandbar/examples/session_demo.clj
http://rjevans.net/post/2628238502/session-support-in-compojure-ring
https://github.com/ring-clojure/ring/wiki/Sessions
These examples do not help me understand how to integrate sessions into something like a login mechanism.
(defroutes main-routes
(POST "/login" request (views/login request)))
;; views.clj
(defn login
[request]
(let [{params :params} request
{username :username} params
{password :password} params
{session :session} request]
(if (db/valid-user? username password)
(-> (logged-in request)
(assoc-in [:session :username] username))
(not-logged-in))))
I realize that this isn't correct as logged-in returns hiccup/html and I believe that the ring response map isn't added on until after the route is fully evaluated. This seems to be why all of the above examples show sessions being added to a complete response map. But, one of the features of Compojure to begin with was abstracting away the requirement of the development having to work with the response map. Therefore I feel like I must me missing something.
What would the correct way be to do the above?
If (logged-in request) returns the contents that should be rendered, then instead of associating :session :username onto the results of logged-in, you can return a proper response map:
{:body (logged-in request)
:session (assoc session :username username)}
:status, :headers, etc. have decent defaults if you do not provide them.
I am writing a web API using Compojure with basic-authentication middleware. The basic authentication part looks something like:
(defn authenticated? [id pass]
(and (= id "blah")
(= pass "blah"))
id and pass are passed in using the id:pass#website technique. My problem is, I would like to access this id and pass further on down where the routes are processed with the various GET, PUT, POST, etc. headings. But I can't figure out how to do this, and haven't had any luck googling around.
; i'd like to access id and pass here
(defroutes routes
(GET "/" [] {:status 200 :headers {} :body "hi!"})
...)
I presume the solution is for the above to somehow "add" id and pass to some set of variables that can be accessed where the routes are processed, but I have no idea how to add nor to access them.
Hopefully someone can point me in the right direction - thanks.
Assuming you are talking about https://github.com/remvee/ring-basic-authentication, the authenticated? fn can return a truthly value that will be added to the request in the :basic-authentication. Something like (untested):
(defn authenticated? [id pass]
(and (= id "gal")
(= pass "foo")
{:user id :passwd pass}))
(defroutes routes
(GET "/" {the-user :basic-authentication} {:status 200 :headers {} :body (str "hi! Mr. " (:user the-user) " your password is " (:passwd the-user))})
...)
The return of the authenticated? method is associated in request map referenced by the key :basic-authentication. Here goes an example with a route that returns the user. You could, however, return a map or any other object and access it through :basic-authentication key.
(defn authenticated? [user password] user)
(defroutes require-auth-routes
(GET "/return-user" request (.toString (:basic-authentication request)))
(def my-app
(routes
(-> require-auth-routes
(wrap-basic-authentication authenticated?)))