skip to content

In REST Assured, how does given().csrf(path) decide to send the token as a header or a form parameter?

level: middleimportance: must knowfreq 36%

answer

  1. an extra GET before the real call
  2. GET and HEAD are skipped
  3. meta tag first, hidden input second
  4. CsrfPrioritization has two values
  5. X-CSRF-TOKEN is the default header

basics

~20 s

REST Assured GETs that path and scans the HTML. By default it prefers a meta tag named _csrf_header and sends the value as the X-CSRF-TOKEN header. Otherwise it uses the hidden input named _csrf as a form parameter.

solid answer

~50 s

`given().csrf(path)` sets `CsrfConfig.csrfTokenPath`, which arms an internal `CsrfFilter`. For any request whose method is not `GET` or `HEAD`, that filter issues its own `GET` of the path with `auth().none()` and the current cookies, then hands the response to `CsrfTokenFinder`. The search order is `CsrfConfig.csrfPrioritization`, an enum with exactly two values that defaults to `HEADER`: REST Assured first looks for `<meta name="_csrf_header" content="...">` and, if it finds one, adds the value as the `X-CSRF-TOKEN` header; only when that is absent does it look for `<input name="_csrf" value="...">` and send the value as a form parameter. `csrfPrioritization(CsrfPrioritization.FORM)` reverses the two. All three names are configurable through `csrfMetaTagName`, `csrfHeaderName` and `csrfInputFieldName`, and `csrf(path, fieldName)` is shorthand for the last. Find neither and the filter throws `IllegalArgumentException`, naming both the input field and the meta tag it looked for.

code

java · 23 lines
java
import io.restassured.config.CsrfConfig;
import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.config;
import static io.restassured.RestAssured.given;

public class CataloguePlaceHoldTest {

    @Test
    void staffCanPlaceAHold() {
        given().
                baseUri("http://localhost:8080").
                config(config().csrfConfig(CsrfConfig.csrfConfig()
                        .csrfPrioritization(CsrfConfig.CsrfPrioritization.FORM))).
                csrf("/catalogue/holds/new", "_csrf").
                formParam("isbn", "9780441013593").
                formParam("branch", "Riverside").
        when().
                post("/catalogue/holds").
        then().
                statusCode(201);
    }
}

go deeper

for a junior

Be ready to say that csrf(path) makes REST Assured fetch that page first and carry the token it finds into your next request, and that disableCsrf() switches the behaviour back off.

for a middle

Explain the search order under the default HEADER prioritization — meta tag then hidden input — the X-CSRF-TOKEN header versus the _csrf form parameter, and how csrfPrioritization(FORM) reverses it.

for a senior

Show that you know the cost and the failure modes: one extra round trip per mutating request, a fresh token needed when the server rotates them, and a loud IllegalArgumentException when neither shape is found.

for a principal

Weigh whether a suite should drive the browser-facing CSRF flow at all or exercise the API through a token path instead, and what that choice does to environment parity and suite runtime.

## What `csrf(path)` actually turns on `RequestSpecification.csrf(String csrfTokenPath)` does not add a header or a parameter itself. It rewrites the specification's config — `csrfConfig(csrfConfig().csrfTokenPath(path))` — and that single setting is what makes CSRF live. `CsrfConfig.isCsrfEnabled()` is literally `csrfTokenPath != null`, which has a consequence worth remembering: `config(config().csrfConfig(csrfConfig()))` with no path leaves CSRF **off**, however deliberate the call looks. The two-argument `csrf(path, fieldName)` is the same thing plus `csrfInputFieldName(fieldName)`. Going the other way, `disableCsrf()` clears the flag that appends the filter and strips any `CsrfFilter` already on the specification. ## The extra request, and which calls pay for it When the specification is built, a `CsrfFilter` is appended to the filter chain. On each call it does the following: 1. It checks the method. `GET` and `HEAD` are skipped outright — those are safe methods that servers do not gate on a token, and a fetch for them would be pure cost. 2. For anything else it builds a fresh specification with `auth().none()`, CSRF disabled (so it cannot recurse) and the current request's cookies, and issues a `GET` of `csrfTokenPath`. 3. It parses that response as HTML and asks `CsrfTokenFinder` for a token. 4. If `automaticallyApplyCookies` is on — it is by default — the cookies from that `GET` are copied onto the real request, and also pushed into a `CookieFilter` or `SessionFilter` if either is on the chain. 5. It attaches the token and calls `ctx.next(...)`, so your real request finally goes out. That is one extra round trip **per mutating request**, which is the price of the feature and the reason the documentation warns it will slow a suite down. ## Meta tag first, hidden input second `CsrfTokenFinder` runs two finders in an order chosen by `CsrfConfig.csrfPrioritization`. The enum `CsrfPrioritization` has exactly two constants, `FORM` and `HEADER`, and the default is `HEADER`. The first finder that returns a token wins; the other is never consulted. | Prioritization | Looked for first | Where the token is sent | Fallback | |---|---|---|---| | `HEADER` (default) | `<meta name="_csrf_header" content="…">` | header `X-CSRF-TOKEN` | the hidden input, sent as a form parameter | | `FORM` | `<input name="_csrf" value="…">` | form parameter `_csrf` | the meta tag, sent as a header | So the placement is not something you choose directly — you choose which shape to look for first, and the shape that is found dictates the placement. A page that carries only one of the two behaves identically under both settings; prioritization only matters when a page offers both and they differ. ## The names you can change Every string in that table is a default on `CsrfConfig`, and every setter returns a new immutable instance: - `csrfInputFieldName(String)` — default `_csrf`, the hidden input searched for. `csrf(path, fieldName)` sets this from the DSL. - `csrfMetaTagName(String)` — default `_csrf_header`, the `name` attribute of the meta tag searched for. - `csrfHeaderName(String)` — default `X-CSRF-TOKEN`, the header the token is written to when the meta tag wins. - `csrfPrioritization(CsrfPrioritization)` — default `HEADER`. - `automaticallyApplyCookies(boolean)` — default `true`; set it false if you do not want the token page's cookies riding along. - `loggingEnabled()` and its `LogDetail` / `LogConfig` overloads — the only way to see the hidden token fetch, since it happens inside a filter. A config with a `csrfTokenPath` can also be set globally, so a whole suite pays for CSRF without repeating `csrf(...)` on every call: `RestAssured.config = config().csrfConfig(csrfConfig().csrfTokenPath("/catalogue/holds/new"))`. ## When it goes wrong The failure modes are narrow and loud: - Neither a matching meta tag nor a matching input is present: `IllegalArgumentException`, naming both the input field name and the meta tag name it looked for and printing the response it got. - The token path is wrong and returns a 404 body: the same exception, because that body contains no token either. - The token is placed in the wrong half of the request: usually a mis-set `csrfMetaTagName`, which lets the meta finder fail silently and the form finder win. - Nothing happens at all: either the request is a `GET`, or `csrfTokenPath` was never set. ## CSRF alongside form login The two features stack, and they each buy their own token. When `auth().form(...)` and CSRF are both configured, the `FormAuthFilter` runs first: it `GET`s the token page, posts the credentials with the token it found, and copies the session cookies onto your request. The `CsrfFilter` then runs and `GET`s the token page **again**, because servers typically rotate the token per request and the one spent on the login `POST` is no longer valid. A `POST` to `/catalogue/holds` with both enabled therefore makes three requests before yours. Note the earlier `FormAuthConfig.withAutoDetectionOfCsrf()` is gone; `csrf(...)`, `disableCsrf()` and `CsrfConfig` are the current surface.

  • How do you turn REST Assured's CSRF handling off for a single request?
    Call `given().disableCsrf()`. It clears the flag that appends the `CsrfFilter` and removes any `CsrfFilter` already on the specification. Remember too that a `CsrfConfig` built without a `csrfTokenPath` is inert — `isCsrfEnabled()` is just `csrfTokenPath != null` — so passing a bare `csrfConfig()` leaves CSRF off rather than switching it on.
  • Why do form login and CSRF each cost their own extra request?
    They need different tokens. The `FormAuthFilter` `GET`s the token page and posts the credentials with the token it found there. The `CsrfFilter` then `GET`s the page again, because servers usually rotate the token per request and the one spent on the login `POST` is dead. A `POST` with both enabled makes three requests before yours.

saying these in an interview costs you the question

  • Thinks csrf(path) adds a token to every request, GETs included
  • Assumes the token always goes out as a form parameter
  • Believes the default header name is X-XSRF-TOKEN
  • Names withAutoDetectionOfCsrf() as the way to enable detection
  • Thinks a bare csrfConfig() with no token path enables CSRF
  • Expects one token fetch to serve every request in the test