skip to content

When is it correct to disable CSRF protection for a stateless bearer-token API, and why is it safe there but dangerous with cookie auth?

level: seniorimportance: must knowfreq 76%

answer

  1. CSRF = ambient/auto-sent credentials only
  2. bearer header is not auto-attached cross-site
  3. Authorization not CORS-simple -> preflight
  4. JWT-in-cookie still needs CSRF
  5. dual chain: disable on bearer chain only

basics

~20 s

Disable CSRF only when auth uses a bearer token in the Authorization header and no cookies. Browsers don't auto-attach that header cross-site, so there's nothing to forge. If auth uses cookies (session or JWT-in-cookie), keep CSRF on.

solid answer

~50 s

CSRF exploits **ambient credentials the browser attaches automatically** — cookies, HTTP Basic. A **stateless API authenticated purely by `Authorization: Bearer <token>`** is not CSRF-vulnerable because the browser never auto-sends that header; the client's JavaScript must attach it explicitly, and an attacker's cross-origin page cannot read your token nor set a cross-origin `Authorization` header (it's not a CORS-simple header). So for a pure bearer API you `http.csrf(csrf -> csrf.disable())` — the check adds friction with no benefit. The danger is disabling CSRF while **any cookie-based authentication remains** (a session cookie, or a JWT stored in a cookie). Then requests still authenticate via the auto-attached cookie, and removing CSRF reopens the hole. Rule of thumb: CSRF protection is needed exactly when the browser can authenticate a request **without your code choosing to**. Mixed apps (cookie for browser, bearer for service clients) should keep CSRF for the cookie-authenticated filter chain and can disable it only on a separate bearer-only chain.

code

java · 20 lines
java
// Two chains: bearer API (CSRF off) and cookie-auth browser app (CSRF on)
@Bean
@Order(1)
SecurityFilterChain api(HttpSecurity http) throws Exception {
    http.securityMatcher("/api/**")
        .authorizeHttpRequests(a -> a.anyRequest().authenticated())
        .csrf(csrf -> csrf.disable())          // safe: bearer in Authorization header, no cookies
        .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
        .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
    return http.build();
}

@Bean
@Order(2)
SecurityFilterChain web(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(a -> a.anyRequest().authenticated())
        .formLogin(Customizer.withDefaults())
        .csrf(Customizer.withDefaults());      // cookie/session auth -> keep CSRF ON
    return http.build();
}

go deeper

for a junior

Know the rule: no cookies + bearer header = safe to disable; cookie auth = keep CSRF.

for a middle

Explain why the Authorization header isn't auto-attached and that JWT-in-cookie still needs CSRF.

for a senior

Design dual filter chains and articulate the ambient-credential principle precisely, including CORS interplay.

for a principal

Reason about mixed auth models, migration risks (moving tokens into cookies), and policy for enforcing per-chain CSRF decisions across teams.

## The governing principle CSRF is only possible when authentication rides on **credentials the browser sends automatically and implicitly**: session cookies, HTTP Basic, TLS client certs. The attacker's page can *cause* a request to your origin, and the browser helpfully attaches those ambient credentials. CSRF tokens close that gap by demanding a secret the attacker cannot obtain. ## Why a pure bearer-token API is immune With `Authorization: Bearer <jwt>` the token is **not ambient**: 1. The browser does **not** automatically attach an `Authorization` header to cross-site requests. Only the client's own JavaScript, running on your origin, sets it. 2. An attacker page on `evil.com` **cannot read** your token — it lives in memory or `localStorage`, blocked by the same-origin policy — so it cannot construct a valid request. 3. `Authorization` is **not a CORS-safelisted header**, so cross-origin attempts to set it trigger a preflight that your CORS config controls. Therefore a request forged by a third-party page arrives **unauthenticated** (no bearer header) and is simply rejected by authentication — CSRF adds nothing. This is why the standard resource-server config disables it: ```java http.csrf(csrf -> csrf.disable()) .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults())); ``` ## Why it's dangerous with cookie auth If your app authenticates via an **HttpSession cookie** or, crucially, a **JWT stored in a cookie**, the browser auto-attaches it on cross-site requests. Disabling CSRF here means a forged POST authenticates successfully — the classic vulnerability. "JWT" does **not** make you CSRF-immune; **where you store it** does. JWT-in-cookie is exactly as CSRF-vulnerable as a session cookie. ## Mixed / dual-chain setups Many systems serve browsers (cookie/session) and machine clients (bearer). Use **multiple `SecurityFilterChain` beans** with `securityMatcher(...)`: - `/api/**` bearer-only chain → `csrf.disable()`. - Everything else (cookie-authenticated) → CSRF **enabled**. Disabling CSRF globally to "fix" the API would strip protection from the cookie chain too. ## Extra hardening even for bearer APIs - **CORS** must still be configured correctly; disabling CSRF is not a license to allow arbitrary origins. - Consider **SameSite** on any cookies present. - If you later move the token into a cookie for convenience, you must **re-enable CSRF**. ## Common interview trap "We use JWT, so we turned CSRF off" is only correct **if** the JWT travels in the `Authorization` header. Always ask: *where is the credential stored?* Header → safe to disable. Cookie → keep CSRF.

  • A team stores their JWT in an HttpOnly cookie and disabled CSRF. Is that safe?
    No. A cookie is auto-attached by the browser cross-site, so the API is CSRF-vulnerable regardless of it being a JWT. They must re-enable CSRF (or move the token to the Authorization header).
  • Does disabling CSRF mean you no longer need CORS configuration?
    No — they solve different problems. CORS governs which origins may read cross-origin responses / make non-simple requests; CSRF governs forged state changes via ambient credentials. A bearer API still needs correct CORS.
  • How do you disable CSRF for only the API without weakening the browser app?
    Define multiple SecurityFilterChain beans with securityMatcher: the /api/** bearer chain calls csrf.disable(), while the cookie-authenticated chain keeps csrf enabled.

saying these in an interview costs you the question

  • "We use JWT so CSRF is unnecessary" without checking the token is in a header, not a cookie
  • Globally disabling CSRF to make an API work while cookie auth still exists
  • Believing HttpOnly on an auth cookie prevents CSRF (it prevents JS reads, not automatic sending)
  • Thinking disabling CSRF removes the need for CORS

context