Test code:
(testing "adding a record"
(let [response (app (mock/request :post "/api/df"
"{\"id\":123}"))]
(prn response)
(is (= (:status response) 200))))
Testing error when prn the response:
{:status 403, :headers {"Content-Type" "text/html; charset=utf-8", "X-XSS-Protection" "1; mode=block", "X-Frame-Options" "SAMEORIGIN", "X-Content-Type-Options" "nosniff"}, :body "<h1>Invalid anti-forgery token</h1>"}
Cross Site Request Forgery, is an attack where an evildooer put's a link on their site that tricks the browser of someone on that site into making a request to your site.
If that someone happens to be logged into your site at the time, then that request will cause things to happen as if they had asked for it (because their browser asked for it to happen). This can be a very serious problem and it affects both GET and POST requests.
The general solution is to make it so no serious actions can happen on the first connection to your site, but rather the first connection sets a token header, that the server expects to see on the next request. This allows the server to verify the chain of requests, and thus prevents CSRF (or XSRF).
It sounds like, if you want your test to make requests to this service, you need your request to first acquire a proper CSRF token, and then make the request it want's to test.
As it stands, your test is testing that this call is not vulnerable to CSRF, so it's a perfectly useful test, you should keep it, and write another that get's a proper token before making the request.
We can disable csrf in tests using (assoc site-defaults :security false). The complete code is something like this:
; Create a copy of testing app in utilities.testing
; by wrapping handler with testing middlewares
(ns utilities.testing
(:require [your-web-app.handler :refer [path-handler]]
[ring.middleware.defaults :refer [wrap-defaults site-defaults]]))
; Disabling CSRF for testing
(def app
(-> path-handler
(wrap-defaults (assoc site-defaults :security false))))
Now you can use this app in tests
(ns users.views-test
(:require [utilities.testing :refer [app]]
;...
))
;...
(testing "adding a record"
(let [response (app (mock/request :post "/api/df"
"{\"id\":123}"))]
(prn response)
(is (= (:status response) 200))))
Related
I'm getting a baffling behavior when using Ring where POST requests all give "Invalid anti-forgery token" but only do this when I have the handler behind a function.
So,
(def app app-routes)
works fine.
Whereas
(defn app [args]
(app-routes args))
throws anti-forgery errors for POST requests.
Minimal code example. :
(defroutes app-routes
(GET "/login" request (fn [req]
(html5
[:body
[:div
[:form {:action "/login" :method "post"}
(anti-forgery-field)
[:input {:name "username"}]
[:input {:name "password"}]
[:button {:type "submit"} "submit"]]]])))
(POST "/login" request (fn [req] "Yay!")))
; WORKS A-OK like this
(def app (-> app-routes (wrap-routes site-defaults)))
In my project.clj
:plugins [[lein-ring "0.12.5"]]
:ring {:handler myapp.application/app
But again, if I change this to be a function:
(defroutes app-routes
(GET "/login" request (fn [req]
(html5
[:body
[:div
[:form {:action "/login" :method "post"}
(anti-forgery-field)
[:input {:name "username"}]
[:input {:name "password"}]
[:button {:type "submit"} "submit"]]]])))
(POST "/login" request (fn [req] "Yay!")))
; ALL POST REQUESTS FAIL
(defn app [args]
(let [handler (-> app-routes (wrap-routes site-defaults)))]
(handler args)))
the all post requests fail due to forgery tokens. All GET requests work fine.
Stranger still: I thought maybe it didn't like being re-initialized, so I stuffed it into an atom to make it more singleton-like
(def handler (atom nil))
(defn app [args]
(swap! handler #(when (nil? %)
(-> app-routes
(wrap-defaults site-defaults))))
(#handler args))
Now GET and POST requests work as expected. Except now wrap-reload (not pictured above) throws exceptions!
I have no idea why it is behaving this way, and have been completely unable to debug. Could anyone shed some light?
Edit:
For context on why I'm trying to wrap it at all: I want to be able to wire up dependencies (like database connections) and then inject them into the controllers used by the routes .
I'm not an expert on wrap-routes, but there are clear clues to your problem.
In method #1 with def, you create a handler and assign it to the var attached to the symbol app.
In method #2 (failing), you define a function app. Since handler is now in the let form, you are creating a new handler on each function call. I'd wager that is the source of the problem.
In method #3, the handler atom is only initialized upon the first call, and never changed after that (because of the nil? test).
Since it works in #1 and #3 when the handler is only created once, it seems clear that creating a new handler on each call in #2 is the problem. Since the anti-forgery token is basically a random number that is different on each call to wrap-routes, you never have the same token used by the POST that was generated by the GET route.
Re your larger goal: You may find it easier to use the Pedestal framework to inject the desired dependencies into the request. Or, you could write a simple Compojure handler (really just a function that takes a handler and returnes a wrapped version of that handler). Probably only 5-10 lines of code.
You will also probably like the book Web Development with Clojure.
Here is a list of other documentation.
wrap-cors does not return access control headers when there is a bad request against my api endpoint. I believe this is because I am using a exception handler which might be blocking the middleware from running. I want to know how I can still execute the middleware for this route and append the cors headers to the response of bad requests.
exception handler
(defn- bad-request-handler
"Handles bad requests."
[f]
(f
(ring/response {:status "bad request"})))
app
(def app
(api
{:exceptions {:handlers
{::ex/request-validation (bad-request-handler response/bad-request)}}}
(POST "/" [] :body [item {(schema/required-key :item) schema/Bool}]
:middleware [#(wrap-cors % :access-control-allow-origin [#".*"]
:access-control-allow-methods [:post])]
:responses {200 {:description "ok"}
400 {:description "bad request"}} item)))
I've decided to append my own headers rather than using r0man/ring-cors.
I can determine the contents of the Access-Control-Allow-Methods by retrieving the :request-method value from the request.
However, this makes the assumption that the bad request handler will only ever be called by valid routes.
(defn- append-cors
"Allow requests from all origins"
[methods]
{"Access-Control-Allow-Origin" "*"
"Access-Control-Allow-Methods" methods})
(defn- bad-request-handler
"Handles bad requests."
[f]
(fn [^Exception e data request]
(->
(f request)
(update-in [:headers] merge (->
(request :request-method)
(name)
(clojure.string/upper-case)
(append-cors)))
(assoc :body {:status "bad request"}))))
I'm still not really sure why the cors headers are only added when the request is allowed.
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'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 have a clojure / compojure webapp with the following routes
(defroutes my-routes
(GET "/app/preview" request (my-preview-function request))
(ANY "*" request (str "ANY page <br>" (request :params))))
The preview GET request is made with a couple of parameters. I find this works most of the time but sometimes the /ebook/preview is not found and processing drops to the ANY route, in which case the output is similar to this,
ANY page
{:* "/app/preview", :section "50", :id "48"}
Can anyone suggest what might cause the /ebook/preview request to be skipped? It is definitely a GET request being made; the HTML does not have a POST for the /app/preview URL and to be doubly sure I added a POST route for /app/preview and that was not being hit.
JAR versions:
Clojure 1.2
compojure-0.6.2
ring-core-0.3.7
jetty-6.1.14
ring-jetty-adapter-0.3.1
ring-servlet-0.3.1jar
servlet-api-2.5-6.1.14
Routes are wrapped as follows
(require '[compojure.handler :as handler])
(defn wrap-charset [handler charset]
(fn [request]
(if-let [response (handler request)]
(if-let [content-type (get-in response [:headers "Content-Type"])]
(if (.contains content-type "charset")
response
(assoc-in response
[:headers "Content-Type"]
(str content-type "; charset=" charset)))
response))))
(def app (-> my-routes
handler/site
wrap-stateful-session
(wrap-charset "utf-8")
(wrap-file "public")))
(defn run []
(run-jetty (var app) {:join? false :port 8080}))
If you're trying to figure out what request is causing the problems, stop throwing away the request map with (request :params) and just have a look at request. That will give you a map with all the information Compojure has; you can inspect it, and pass it back into your routes later to observe what happens (after you make some changes, say).
If
(my-preview-function request)
returns nil, then the routing will try the next route. Take a look at (source GET) and see how it matches (or doesn't) your route.