Search code examples
clojurecsrfcompojureringreagent

Compojure app not sending CSRF by default


I'm using reagent and compojure to make a toy webapp and I can't figure out why my server isn't sending out a CSRF cookie. Other answers and several blog posts seem to imply that the default settings for compojure now send the CSRF token and that manually resending it is actually a bug. When I try to hit the POST /art endpoint I get back a 403 Forbidden response. None of the pages get the cookie with the CSRF token in it so I can't send it with the POST request. Any advice?

;;server.clj

(ns my-app.server
  (:require [my-app.handler :refer [app]]
            [environ.core :refer [env]]
            [ring.adapter.jetty :refer [run-jetty]])
  (:gen-class))

 (defn -main [& args]
   (let [port (Integer/parseInt (or (env :port) "3000"))]
     (run-jetty app {:port port :join? false})))

;; handler.clj

(ns my-app.handler
  (:require [compojure.core :refer [GET POST defroutes]]
            [compojure.route :refer [not-found resources]]
            [hiccup.page :refer [include-js include-css html5]]
            [my-app.middleware :refer [wrap-middleware]]
            [environ.core :refer [env]]))


(defroutes routes
  (GET "/" [] loading-page)
  (GET "/about" [] loading-page)
  (GET "/art" [] loading-page)
  (POST "/art" request {:sent (:body request) :hello "world"})

  (resources "/")
  (not-found "Not Found"))

(def app (wrap-middleware #'routes))


;;middleware.clj

(ns stagistry.middleware
  (:require [ring.middleware.defaults :refer [site-defaults wrap-defaults]]
            [prone.middleware :refer [wrap-exceptions]]
            [ring.middleware.reload :refer [wrap-reload]]))

(defn wrap-middleware [handler]
  (-> handler
      (wrap-defaults site-defaults)
      wrap-exceptions
      wrap-reload))

I threw the code itself on github here since I still can't see what's wrong.


Solution

  • Other answers and several blog posts seem to imply that the default settings for compojure now send the CSRF token and that manually resending it is actually a bug.

    (wrap-defaults site-defaults) applies the ring-anti-forgery middleware. This will only add a CSRF token to each ring browser session and look for the token on POST requests. If the token is missing the middleware will return a 403 for the request. Adding the token to your form or ajax/whatever post requests is up to you, the price of freedom. :)

    From the ring-anti-forgery docs:

    By default, the token is expected to be in a form field named '__anti-forgery-token', or in the 'X-CSRF-Token' or 'X-XSRF-Token' headers.

    For example try adding this route:

    (GET "/someform" request (html5 (ring.util.anti-forgery/anti-forgery-field)))
    

    The anti-forgery-field helper will add a hidden input field with the CSRF token as its value, which is picked up by the middleware if the form is posted. To access the token directly you can either use ring.middleware.anti-forgery/*anti-forgery-token* or look it up in the session of the current request map:

    (-> request :session :ring.middleware.anti-forgery/anti-forgery-token)
    

    The global var (and by extension the helper) is bound to the handler context though, you can't access it from outside or from another thread in the same context.

    Simple curl header example:

    1. Get the CSRF token:

      $ curl -v -c /tmp/cookiestore.txt http://localhost:3000/someform
      

    2. Set the token via header and post some stuff:

      $ curl -v -b /tmp/cookiestore.txt --header "X-CSRF-Token: ->token from prev. req<-" -X POST -d '{:foo "bar"}' localhost:3000/art