Environment
Frontend (FE) is React 18, backend (BE) is SpringBoot 3
Issue:
SpringBoot ignores XSRF-TOKEN
header/cookie and returns HTTP 403.
Moreover, the returned error 403 contains no more info, no description. Server log is also empty.
Motivation
I have JWT as httpOnly cookie for user authorization. However, such cookie should be protected against CSRF. Therefore, I am trying to add csrf-token in local storage and send it via every non-GET request in a header.
MWE/Implementation
https://github.com/Engin1980/csrf-mwe
Controller (BE)
@RestController
public class AppController {
@GetMapping
public void get(){
// blank
}
@PostMapping
public void post(){
// blank
}
}
WebSecurity (BE)
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class WebSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
CookieCsrfTokenRepository cookieCsrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
cookieCsrfTokenRepository.setCookieCustomizer(q -> q.sameSite("strict"));
http.csrf(q -> q.csrfTokenRepository(cookieCsrfTokenRepository).csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()));
http.addFilterAfter(new CsrfCookieFilter(), CsrfFilter.class);
http.addFilterBefore(new CsrfCheckFilter(), CsrfFilter.class);
http.cors(q -> q.disable());
http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.authorizeHttpRequests(auth -> auth.requestMatchers("/**").permitAll());
return http.build();
}
}
final class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler {
private final CsrfTokenRequestHandler delegate = new XorCsrfTokenRequestAttributeHandler();
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
this.delegate.handle(request, response, csrfToken);
}
@Override
public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
if (StringUtils.hasText(request.getHeader(csrfToken.getHeaderName()))) {
return super.resolveCsrfTokenValue(request, csrfToken);
}
return this.delegate.resolveCsrfTokenValue(request, csrfToken);
}
}
final class CsrfCookieFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
CsrfToken csrfToken = (CsrfToken) request.getAttribute("_csrf");
csrfToken.getToken();
filterChain.doFilter(request, response);
}
}
final class CsrfCheckFilter extends OncePerRequestFilter {
private final Logger logger = LoggerFactory.getLogger(CsrfCheckFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String key;
key = "XSRF-TOKEN";
logger.info("Header {}={}", key, request.getHeader(key));
key = "X-CSRF-TOKEN";
logger.info("Header {}={}", key, request.getHeader(key));
filterChain.doFilter(request, response);
}
}
React App Component (FE)
import React from "react";
import logo from "./logo.svg";
import "./App.css";
import axios from "axios";
function App() {
axios.defaults.baseURL = "http://localhost:5555";
axios.defaults.withCredentials = true;
axios.interceptors.request.use((c) => {
const csrf: string | null = localStorage.getItem("csrf");
if (csrf) {
console.log("inter.req setting csrf " + csrf);
c.headers["XSRF-TOKEN"] = csrf;
c.headers["X-CSRF-TOKEN"] = csrf;
}
return c;
});
const getClick = () => {
axios
.get("")
.then((q) => {
console.log("GET completed");
console.log("Cookies: " + document.cookie);
const csrf = document.cookie
.split(";")
.filter((q) => q.startsWith("XSRF-TOKEN"))?.[0]
?.split("=")?.[1];
console.log("CSRF token: " + csrf);
if (csrf) localStorage.setItem("csrf", csrf);
})
.catch((e) => {
console.error("GET failed");
console.error(e);
});
};
const postClick = () => {
axios.post("").then((q) => {
console.log("POST completed");
});
};
return (
<div className="App">
<h1>CSRF test</h1>
<input type="button" value="GET" onClick={getClick} />
<input type="button" value="POST" onClick={postClick} />
</div>
);
}
export default App;
From spring-security docs:
The CookieCsrfTokenRepository writes to a cookie named XSRF-TOKEN and reads it from an HTTP request header named X-XSRF-TOKEN or the request parameter _csrf by default. These defaults come from Angular and its predecessor AngularJS.
Hence, please try to change c.headers["XSRF-TOKEN"] = csrf;
to c.headers["X-XSRF-TOKEN"] = csrf;