I just migrated my spring boot app to 3.2 from 2.7. Right now everything is working fine login, request which i make through axios as i am using vue js. But when i download file via form element using javascript(I am downloading pdf of a page using puppeteer) which is a post request it throws and error crsf token is null. I am unable to find what i am missing in security configuration.
{
"title":"Something Went Wrong",
"status":500,
"detail":"Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-XSRF-TOKEN'."
}
My request payload look like this
values:{
"htmlContent":"some html content for which i want to generate pdf",
"config":{}
}
_csrf: crsf token
t: some integer value
My Javascript method to download file is
function downloadFile(url, method, data, transactionId) {
const form = createElement('FORM');
form.method = method;
form.action = url;
const input = createElement('INPUT');
input.id = 'values';
input.name = 'values';
input.type = 'hidden';
input.value = JSON.stringify(data);
form.appendChild(input);
const csrfInput = createElement('INPUT');
csrfInput.id = '_csrf';
csrfInput.name = '_csrf';
csrfInput.type = 'hidden';
debugger;
csrfInput.value = Cookies.get('XSRF-TOKEN');
form.appendChild(csrfInput);
if (transactionId) {
const transactionIdInput = createElement('INPUT');
transactionIdInput.id = 't';
transactionIdInput.name = 't';
transactionIdInput.type = 'hidden';
transactionIdInput.value = transactionId;
form.appendChild(transactionIdInput);
}
document.body.appendChild(form);
form.submit();
form.parentNode.removeChild(form);
}
Csrf Token Request Handler taken from spring docs
final class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler {
private final CsrfTokenRequestHandler delegate = new XorCsrfTokenRequestAttributeHandler();
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
/*
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
* the CsrfToken when it is rendered in the response body.
*/
this.delegate.handle(request, response, csrfToken);
}
@Override
public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
/*
* If the request contains a request header, use CsrfTokenRequestAttributeHandler
* to resolve the CsrfToken. This applies when a single-page application includes
* the header value automatically, which was obtained via a cookie containing the
* raw CsrfToken.
*/
if (StringUtils.isNotBlank(request.getHeader(csrfToken.getHeaderName()))) {
return super.resolveCsrfTokenValue(request, csrfToken);
}
/*
* In all other cases (e.g. if the request contains a request parameter), use
* XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
* when a server-side rendered form includes the _csrf request parameter as a
* hidden input.
*/
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");
// Render the token value to a cookie by causing the deferred token to be loaded
csrfToken.getToken();
filterChain.doFilter(request, response);
}
}
Security Config looks like this
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.sessionManagement(management -> management.maximumSessions(-1).sessionRegistry(sessionRegistry()).expiredUrl("/api/expire-me"))
.csrf(csrf -> {
CookieCsrfTokenRepository csrfRepo = CookieCsrfTokenRepository.withHttpOnlyFalse();
SpaCsrfTokenRequestHandler requestHandler = new SpaCsrfTokenRequestHandler();
csrf.requireCsrfProtectionMatcher(new CsrfSecurityRequestMatcher()).csrfTokenRepository(csrfRepo).csrfTokenRequestHandler(requestHandler);
}).addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)...
}
Edit: Using fetch worked because i can attach csrf token to the header
function downloadFile(url, method, data, transactionId) {
const headers = new Headers({
'Content-Type': 'application/x-www-form-urlencoded',
'X-XSRF-TOKEN': Cookies.get('XSRF-TOKEN')
});
const reqParams = new URLSearchParams();
reqParams.append('t', transactionId);
reqParams.append('values', JSON.stringify(data));
fetch(url, {
method: 'POST', // *GET, POST, PUT, DELETE, etc.
credentials: 'same-origin', // include, *same-origin, omit
redirect: 'follow', // manual, *follow, error
referrerPolicy: 'strict-origin-when-cross-origin', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
body: reqParams, // body data type must match "Content-Type" header
headers: headers
})
.then((response) => response.blob())
.then((blob) => {
// Create blob URL
const blobUrl = window.URL.createObjectURL(blob);
// Create a temporary anchor el
const anchorEl = document.createElement('a');
anchorEl.href = blobUrl;
anchorEl.download = `history.pdf`; // Set any filename and extension
anchorEl.style.display = 'none';
// Append the a tag to the DOM and click it to trigger download
document.body.appendChild(anchorEl);
anchorEl.click();
// Clean up
document.body.removeChild(anchorEl);
window.URL.revokeObjectURL(blobUrl);
})
.catch((error) => {
console.error('Error downloading file:', error);
});
}