Compojure: access request params from inside basic-auth function? - clojure

I'm using the ring basic-authentication library available for compojure. The authenticated? function takes a username and a password in order to authenticate, but in my particular case I need to access other parameters passed in by the user request besides a username and a password.
For instance, consider a case where there are multiple servers, and a user has an account on a particular server. That user would therefore need to authenticate with (log-on to) a particular server. Therefore I need his username, password, AND server to do the authentication.
Handling a "normal" case which just calls for a username and password might look something like this (using example sql to hit a database):
; curl my_id:my_pass#localhost/my_request
(defn authenticated? [id password]
(first (select user (where {:id id :password password}) (limit 1))))
I'd like to do something like this:
; curl my_id:my_pass#localhost/my_server/my_request
(defn authenticated? [id password ??server??]
(first (select user (where {:server server :id id :password password}) (limit 1))))
I guess my question is, how do I access all request params from inside authenticated? Or, alternatively, how do I pass the server id into the authenticated? function?
Thanks.

In accordance with the comments to the question above, the approach would look like this (just a sketch, I haven't tested whether this works due to lack of a running setup with ring):
(defn wrap-basic-auth-server [handler authfn]
(fn [request]
(-> request
(wrap-basic-authentication (partial authfn (get-in request [:params :server])))
handler)))
What's happening here is that the code is assuming that your server url param will have been added by wrap-params (from ring.middleware.params) to the request map. wrap-basic-authentication is called then with the handler (typical ring middleware, i.e. any other wrapper / handler coming afterwards) and a new (partial) function, which is just your authenticate function which has already swallowed the server arg.
And then instead of just calling wrap-basic-authentication in your routes, you need to add wrap-params and wrap-basic-auth-server and you need your new auth function. E.g.
(defn authenticated? [server user pass]
... your code ...)
(def app
(-> app-routes
... potential other wrappers elided ...
(wrap-params)
(wrap-basic-auth-server authenticated?)))
As I said, I've not tested this code, but it should get you started.

Related

Compojure Ring Middleware How To Use Value Outside of Middleware

I have a simple route and middleware setup with compojure/swagger that is utilizing a ring middleware.
(POST "/worlds" [request]
:body-params [name :- String]
:header-params [token :- String]
:middleware [wrap-api-auth]
:summary "Creates a new world with 'name'"
(ok (do-something-with-user)
(defn wrap-api-auth [handler]
(fn [request]
(let
[token (get (:headers request) "token")
user (db/get-user-by-token token)]
(if user
(handler request) ; pass to wrapped handler
(unauthorized {:error "unauthorized"})))))
This defines a simple route with some basic auth. The token passed in the header param is used to make a database query, which either returns a user and continues, or returns false and fails.
What I'm trying to accomplish is to pass the returned user back out so that I can use it later. I haven't had any luck, as I don't really know where I would try to add it to that I could access it later. I've tried to assoc it with the request but it doesn't appear that I can access it later. The ideal situation is I'm able to pass it to the do-something-with-user function.
Using assoc to add some data to the request should totally work.
You can find an example with some code that is very close to what I have in production at https://gist.github.com/ska2342/4567b02531ff611db6a1208ebd4316e6#file-gh-validation-clj-L124
In essence, that middleware calls
(handler (assoc request
:validation {:valid true
:validation valid?}))
So for your case, the following should just work:
(handler (assoc request
:user user))
If I understood correctly, the destructuring syntax you use is from compojure-api. According to the example at https://github.com/metosin/compojure-api/wiki/Middleware I'd say that the middleware set via the :middleware key behaves just as expected and you should be able to extract the :user from the request that ultimately ends up in your route.
So, just pass the request on to the do-something-with-user function:
(POST "/worlds" request
:body-params [name :- String]
:header-params [token :- String]
:middleware [wrap-api-auth]
:summary "Creates a new world with 'name'"
(ok (do-something-with-user request))
It should contain the :user you assoced into it in your middleware function. Note the missing brackets around request like mentioned in the comments to this answer.

Accessing session data in ring middleware

I'm trying to access basic session data in a ring middleware to, in my case, copy user data into the request for easier access.
(def app
(->
(handler/site app-routes)
(wrap-user-auth) ; <--- my middleware
(session/wrap-session {:store (cookie-store {:key "something super secret"})})))
Middleware:
(defn wrap-user-auth [handler]
(fn [request]
(println request )
(let [user (get (request :session) :user nil)]
(let [response
(handler
(if-not (nil? user)
(assoc request :user user :is_authenticated? true)
(assoc request :user nil :is_authenticated? false)))]
response
))))
Session data is always empty when printing request, even though it has been set and is accessible through the views. I tried messing around with middleware order but can't figure out why the atom is empty.
Some random route where session data is available
(ANY "/foo" request (println (request :session)))
Found a solution.
Turns out this problem is 'somewhat' related to Compojure/Ring: Why doesn't a session with cookie-store survive a server restart?, which explains that 2 session middleware are being used: One by compojure, one by wrap-session. After changing both middleware to the same storage engine, the atom is filled with my session data - although I don't quite understand why.
I am putting this as an answer rather than a comment since it solves the actual problem. Happy to hear why this is happening though!

Sessions in Ring/Compojure

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.

Datomic with friend authentication not working properly

I'm working on a clojure web application project for my college,and i am trying to connect datomic database with friend authentication but is kinda buggy...i will explain further...
First i am doing user registration(user insertion in datomic database) like this and it is working.
(defn insert-user [firstname lastname email password sex date] (.get (.transact conn
[{
:db/id #db/id[:db.part/user -1000001]
:user/name firstname
:user/lastName lastname
:user/username email
:user/password (creds/hash-bcrypt password)
:user/gender sex
:user/birthDate date}
]))
(resp/redirect "/")
)
The routes handler and friend authenticator looks like this...function main is to start the app.
(def page (handler/site
(friend/authenticate
routes
{:allow-anon? true
:login-uri "/login"
:default-landing-uri "/login"
:unauthorized-handler #(-> (html5 [:h2 "You do not have sufficient privileges to access " (:uri %)])
resp/response
(resp/status 401))
:credential-fn (partial creds/bcrypt-credential-fn users)
:workflows [(workflows/interactive-form)]})
(wrap-keyword-params routes)
(wrap-nested-params routes)
(wrap-params routes)
))
(defn -main []
(run-jetty page {:port 8080 :join? false}))
And for the end the datomic query for users to match with creds/bcrypt-credential-fn function of friend.
(defn upit-korisnici []
(def temp (d/q '[:find ?u ?p
:where [?user :user/username ?u]
[?user :user/password ?p]
]
(d/db conn)))
(def users (into {} (map (fn [[k v]] [k {:username k :password v}]) temp)))
users
)
The thing that is bugging me and leaving me helpless is that when i register(insert user),the user is inserted in datomic database but when i try to log in i can't.It says wrong email and password but the new user is there.When i restart the whole app and try to login with the new users credentials it goes through and logs on.Does anyone know how to solve this problem?
Edit:
I solved this problem by changing :credential-fn (partial creds/bcrypt-credential-fn users) to :credential-fn #(creds/bcrypt-credential-fn users %).
You seem to think it will automatically update your user data, but it isn't going to because user is not a function it is plain data. What happens is (def user... is run then the results are bound to the name user, you are not binding the computation so the data is never updated. You make a similar mistake for temp. The query is run once then the results are bound to temp, and never reevaluated. You should bind them to a function so that it revaluates.
I started working on an UI for the friend lib with datomic persistence: https://github.com/sveri/friend-ui/ You can have a look at it, maybe it solves your problem already, of course you can take code from it, add pull requests / whatever. When I have time I will implement whatever is needed.
Currently it supports:
Sign up
Login
Logout
Twitter Bootstrap support for the templates
It would be nice if we could bundle the work done, as this will be something that many people will need in future.

How to implement user authentication using clojure-liberator?

I'm not really understanding https://github.com/clojure-liberator/liberator and the list of decision points that it provides to the developer. How would one implement a basic auth/auth service using/alongside/on-top-of the library?
The idiomatic way is to implement the :authorized? decision point. However there is currently no support for the handling of basic or digest authentication. A practical approach is to use ring-basic-authentication for authentication and handle only authorization in the resource. The following example uses ring-basic-authentication and sets the token to a users's role. This role is then checked by liberator in authorized?
(defresource admin-only
:handle-ok "secrect"
:handle-unauthorized "for admins only"
:authorized? (fn [{{token :token} :request}]
(= "admin" token)))
;; token returned encodes role
(defn authenticated? [name pass]
(cond (and (= name "scott")
(= pass "tiger")) "admin")
(and (= name "jack")
(= pass "jill")) "user)))
(def app (wrap-basic-authentication admin-only authenticated?))
from the readme"
Resources are compatible with Ring and can be wrapped in Ring middleware. When evaluated, a resource returns a function which takes a Ring request and returns a Ring response.
so you can then wrap it in ring-basic-authentication
(use 'ring.middleware.basic-authentication)
(defn authenticated? [name pass] (and (= name "foo") (= pass "bar")))
(def app (-> routes .. (wrap-basic-authentication authenticated?))