Injecting CSRF token control into form in Chestnut re-frame generated app


#1

I know this is more of a re-frame question, but I asked on the Clojurians Slack and someone said they thought this was specific to Chestnut, so I’m chasing it down here.

How do I add a CSRF token control to a form? This is for a re-frame app generated by Chestnut.


#2

On a quick glance through the Chestnut readme, it has an option for site-middleware using ring default middleware.

Are you trying to use CSRF in your existing project or trying to disable it or work around it?


#3

Use it. I’m currently using site-middleware, so form POSTs without the token are 403’d.

I don’t yet have auth/auth. But it seems like what needs to happen, given the default ring-anti-forgery strategy, is for each GET of index.html to create a new session, for the browser to both write the cookie as well as initialize the re-frame state with the CSRF token upon JS load. Forms will need a hidden field with the token.


#4

In simple cases I just serialize an edn map server side in JSON and write it to the HTML document. This serves as the initial config for the client, and if I’m using CSRF, that token is being filled from ring.middleware.anti-forgery/*anti-forgery-token* (only available or ‘bound’ during the request).

In more complex cases where there’s lots of dynamic things going on before the client initializes, I add an API endpoint (e.g. /api/hello) which returns said data.

As far as I know, for SPAs this is kinda the way to do it: get the CSRF token (and potentially other secrets) to the client somehow – use whatever you find appropriate!

Update: Also, a somewhat neat trick if you write some data to the HTML document is that you can have the script element remove itself from the DOM after initializing:

<script>
  window._app_init_data = '<serialized json>';
  document.currentScript && document.currentScript.remove();
</script>

Which of course will still be visible when inspecting the raw HTML response, but at least it doesn’t leave a bunch of potentially sensitive data in the DOM. You can also unset the __app_init_data key from the global context as soon as your app’s done. None of this is giving you any real security, of course, but it does feel good to not just leave a trail of data through every user’s browser.


#5

@madbonkey is right, you need to get the token for the current session to the client. This is usually done by putting it into the initial HTML payload. Then whenever you submit an AJAX response you put that in a X-XSRF-Token request header or as part of the form-params. Ring-anti-forgery understands both.

This isn’t really Chestnut specific, it’s just something that Chestnut does not handle for you, since Chestnut has no opinion on how you set up your client/server communication. You do need to make sure you use +site-middleware, or edit the resulting configuration to make sure ring-anti-forgery is included, but it seems you got that part since you’re getting 403s.

I tend to put the token in a meta tag in the header, in hiccup:

[:head 
  ...
  [:meta {:name "csrf-token" :content ring.middleware.anti-forgery/*anti-forgery-token*}]]

Then have something like this

(defn meta-value [name]
  (.. js/document
      (querySelector (str "meta[name='" name "']"))
      (getAttribute "content")))

(defn csrf-token []
  (meta-value "csrf-token"))

(defn POST [url opts]
  (cljs-ajax/POST url (assoc-in opts [:headers "X-CSRF-Token"] (csrf-token))))

#6

Thanks! That makes sense. I’m leaning towards using an “/init” endpoint, just to have something reusable for when the user logs in.