I'm still learning Clojure (and all those accompanying libraries...), so if I do anything stupid in my ignorance, feel free to point it out :-)
I have trouble calling REST endpoints via the POST
method from client code. My routes are wrapped using (ring.middleware.defaults/wrap-defaults <my-routes> site-defaults)
(I believe this is a fairly good idea if ever such code is to run in production). This wrapper applies various other wrappers, including ring.middleware.anti-forgery/wrap-anti-forgery
, which implements (among others) a Cross-Site Request Forgery (CSRF or XSRF) prevention scheme - the default (which I'm using) is a synchronizer token (or session) strategy.
Calling the same REST endpoint via GET
works just fine (because CSRF protection is not applied to GET
, HEAD
and OPTIONS
calls - see ring.middleware.anti-forgery/get-request?
), but using POST
(or one of the other methods) results in a 403 - Invalid anti-forgery token response.
(As seen in the sample code below) I do know how to add a "X-CSRF-Token" or "X-XSRF-Token" header to the HTTP request. (Since this is a REST call, I do not add a hidden "__anti-forgery-token" field as suggested by this question and answer, although either one of the headers or the form field would suffice for the wrapper - see ring.middleware.anti-forgery/default-request-token
.) Rather, if I understand the code correctly, my problem stems from the fact that the default strategy compares the above token to a a session token value, that is retrieved by ring.middleware.anti-forgery.session/session-token
:
(defn- session-token [request]
(get-in request [:session :ring.middleware.anti-forgery/anti-forgery-token]))
I have no idea how to set up the session information correctly in the HTTP client call. (Any HTTP client is sufficient, as the above-mentioned 403 result is generated by the middleware wrapper. For the below demo I thus use the simple ring.mock.request
.)
Here is some minimal code that shows what I have up to now. It defines the routes and handlers, then tries to call them from a unit test.
(ns question.rest
(:require [compojure.core :refer :all]
[ring.middleware.defaults :refer [wrap-defaults site-defaults secure-site-defaults]]
[ring.middleware.anti-forgery :refer [*anti-forgery-token*]]
[clojure.test :refer :all]
[ring.mock.request :as mock]))
(defroutes
exmpl-routes
(ANY "/" [] "Site up OK.")
(GET "/aft" [] (force *anti-forgery-token*)))
(def exmpl (wrap-defaults exmpl-routes site-defaults))
(deftest test-mock-fail
(testing "POST to root route"
(let [
; In a normal web app, the view/page would be GET'ed from the server, which would
; include the Anti-Forgery Token in it, and have the POST as an action on it. Hence
; the way atf is done here...
aft (:body (exmpl (mock/request :get "https://localhost:8443/aft")))
request (-> (mock/request :post "https://localhost:8443/")
(mock/header "X-CSRF-Token" aft))
_ (println request)
response (exmpl request)
_ (println response)
]
(is (= 200 (:status response))) ;;403
(is (= "Site up OK." (:body response)))))) ;;Invalid anti-forgery token
The (println)
calls show the following (some formatting applied):
Request:
{ :protocol "HTTP/1.1",
:server-port 8443,
:server-name "localhost",
:remote-addr "localhost",
:uri "/post",
:scheme :https,
:request-method :post,
:headers { "host" "localhost:8443",
"x-csrf-token" "<long token value here>" } }
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>" }
Tutorials I could find mostly concentrate on the GET
method and seem to assume that the endpoints/routes would be called from HTML, which is served from the server (which includes session info). So I feel a bit stuck at the moment.
For a REST API you should use api-defaults
or secure-api-defaults
NOT site-defaults
.
The anti-forgery machinery is designed for web sites where the app is generating a form
and can include the generated token to be sent back as part of the form submission POST
-- it is not intended for use with a REST API.