Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing POST route with anti-forgery and ring-mock

I want to write a test for a simple POST request using ring.mock - something like this:

(testing "id post route"
    (let [response (app (mock/request :post "/" {:id "Foo"}))]
      (is (= 302 (:status response)))))

However, since I use the wrap-csrf middleware I get a 403 status response since I don't provide an anti-forgery token.

Is there a way to write POST tests with ring.mock without disabling the wrap-csrf middleware?

like image 962
Robert Avatar asked Mar 14 '16 11:03

Robert


2 Answers

First, I'd like to echo some of the points about the approach and reasoning by another post here:

  1. CSRF tokens are only necessary for HTML logins, but not needed or desirable for web services.
  2. With Ring/Compojure, it is possible to test handlers directly, or to wrap them in only the middleware necessary. In the example below, I require app which represents the entire application, but you can circumvent the CSRF in order to test endpoints directly if desirable. This would allow you to test the given endpoint, but wouldn't test that the CSRF protection is working correctly.

Assuming the above, you might need to test that the login procedure is working correctly and that CSRF is integrated. The following test helpers should help you along this path:

(ns myapp.test.handler-test
  (:require [clojure.test :refer :all]
            [ring.mock.request :refer [request]]
            [net.cgrand.enlive-html :as html]
            [myapp.handler :refer [app]]))

(defn get-session
  "Given a response, grab out just the key=value of the ring session"
  [resp]
  (let [headers (:headers resp)
        cookies (get headers "Set-Cookie")
        session-cookies (first (filter #(.startsWith % "ring-session") cookies))
        session-pair (first (clojure.string/split session-cookies #";"))]
    session-pair))

(defn get-csrf-field
  "Given an HTML response, parse the body for an anti-forgery input field"
  [resp]
  (-> (html/select (html/html-snippet (:body resp)) [:input#__anti-forgery-token])
      first
      (get-in [:attrs :value])))

(defn get-login-session!
  "Fetch a login page and return the associated session and csrf token"
  []
  (let [resp (app (request :get "/login"))]
    {:session (get-session resp)
     :csrf (get-csrf-field resp)}))

(defn login!
  "Login a user given a username and password"
  [username password]
  (let [{:keys [csrf session]} (get-login-session!)
        req (-> (request :post "/login")
                (assoc :headers {"cookie" session})
                (assoc :params {:username username
                                :password password})
                (assoc :form-params {"__anti-forgery-token" csrf}))]
    (app req)
    session))

In the above, I'm assuming that the /login page uses the __anti-forgery-token hidden input and that you want to test against this. You might also consider placing the CSRF token in the session data, which makes it easier to test with tools like curl which can save session data from one response to a file for use in successive requests.

Since I'm pulling the token out of the HTML body, I decided to use enlive so that I could use a CSS selector to define where I'm pulling this data from in a straightforward and declarative way.

To make use of the above, you can call login! and then use the session data it returns in successive requests that you want to be authenticated with that same session, such as:

 (deftest test-home-page-authentication
  (testing "authenticated response"
    (let [session (login! "bob" "bob")
          request (-> (request :get "/")
                      (assoc :headers {"cookie" session}))]
      (is (= 200 (:status (app request)))))))

This assumes you have a username/password of bob and bob set up in whatever auth backend you are using. You'll likely need to have a username/password setup step that creates this user before this login procedure will work.

like image 68
csmith Avatar answered Nov 15 '22 23:11

csmith


What do you want to test? It's not clear from your question and it's not clear why you shouldn't disable anti-forgery middleware at all.

  1. If you are testing a web service you shouldn't use CSRF tokens at all and switch to a different security mechanism (e.g. authorization headers, API tokens etc.)

  2. If you want to test end-to-end flow including CSRF logic, then you need to obtain a valid CSRF token by calling appropriate URL first and extracting it from the response (e.g. parsing the hidden field) along with the session ID so you can use them in the test request.

  3. If you want to test your handler logic then test it without the wrapping "infrastructure" middleware. There is no point in mocking anti-forgery middleware if you can just not apply it to your handler function in your tests and the problem disappears.

like image 23
Piotrek Bzdyl Avatar answered Nov 15 '22 23:11

Piotrek Bzdyl