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?
answer
- CSRF = ambient/auto-sent credentials only
- bearer header is not auto-attached cross-site
- Authorization not CORS-simple -> preflight
- JWT-in-cookie still needs CSRF
- dual chain: disable on bearer chain only
basics
~20 sDisable 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 sCSRF 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// 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
Know the rule: no cookies + bearer header = safe to disable; cookie auth = keep CSRF.
Explain why the Authorization header isn't auto-attached and that JWT-in-cookie still needs CSRF.
Design dual filter chains and articulate the ambient-credential principle precisely, including CORS interplay.
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