CSRF issues in HTTP POST requests

Hi, I am trying to implement some REST endpoints in my application. I am facing now the issue that my POST, PUT and DELETE requests are responding with a HTML file generated from vaadin instead of a JSON.

What i did already is setting
logging.level.org.springframework.security=DEBUG and vaadin.exclude-urls=/api/*
in my application.properties and disabling csrf in my SecurityConfig

If I use http.csrf(AbstractHttpConfigurer::disable) i am getting “Invalid CSRF token found” in the log.
If I use http.csrf(csrf -> csrf.ignoringRequestMatchers(PathPatternRequestMatcher.withDefaults().matcher("/api/**"))); the Invalid CSRF token error does not occur but i still get the HTML file as response.
Any help would be really appreciated.

Thanks :)

@Configuration
@EnableWebSecurity
public class SecurityConfig extends VaadinWebSecurity {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(auth -> auth
                        .requestMatchers("/api/**").hasAuthority(Role.ROLE_ADMIN.getAuthority())
                        .requestMatchers(PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.GET, "/images/*.png")).permitAll()
                )
//                .csrf(AbstractHttpConfigurer::disable)
                .csrf(csrf -> csrf.ignoringRequestMatchers(PathPatternRequestMatcher.withDefaults().matcher("/api/**")));

        super.configure(http);
        setLoginView(http, LoginView.class);
    }
}
<!doctype html>
<!--
This file is auto-generated by Vaadin.
-->
<html lang="de" theme="dark">

<head>
	<script initial="">
		window.Vaadin = window.Vaadin || {};window.Vaadin.VaadinLicenseChecker = {  maybeCheck: (productInfo) => {  }};window.Vaadin.devTools = window.Vaadin.devTools || {};window.Vaadin.devTools.createdCvdlElements = window.Vaadin.devTools.createdCvdlElements || [];window.Vaadin.originalCustomElementDefineFn = window.Vaadin.originalCustomElementDefineFn || window.customElements.define;window.customElements.define = function (tagName, constructor, ...args) {const { cvdlName, version } = constructor;if (cvdlName && version) {  const { connectedCallback } = constructor.prototype;  constructor.prototype.connectedCallback = function () {    window.Vaadin.devTools.createdCvdlElements.push(this);    if (connectedCallback) {      connectedCallback.call(this);    }  }}window.Vaadin.originalCustomElementDefineFn.call(this, tagName, constructor, ...args);};
	</script>
	<script initial="">
		window.Vaadin = window.Vaadin || {};window.Vaadin.ConsoleErrors = window.Vaadin.ConsoleErrors || [];const browserConsoleError = window.console.error.bind(window.console);console.error = (...args) => {    browserConsoleError(...args);    window.Vaadin.ConsoleErrors.push(args);};window.onerror = (message, source, lineno, colno, error) => {const location=source+':'+lineno+':'+colno;window.Vaadin.ConsoleErrors.push([message, '('+location+')']);};window.addEventListener('unhandledrejection', e => {    window.Vaadin.ConsoleErrors.push([e.reason]);});
	</script>
	<script initial="">
		window.Vaadin.devToolsPlugins = [];
window.Vaadin.devToolsConf = {"enable":true,"url":"./VAADIN/push","backend":"SPRING_BOOT_DEVTOOLS","liveReloadPort":35729,"token":"a4aff85c-88e6-43eb-83c2-3e79814aa908"};
	</script>
	<script initial="">
		window.Vaadin = window.Vaadin || {};
window.Vaadin.developmentMode = true;
	</script>
	<script initial="">
		if (!('CSSLayerBlockRule' in window)) {
    window.location.search='v-r=oldbrowser';
}
	</script>
	<script initial="">
		window.Vaadin = window.Vaadin || {};window.Vaadin.TypeScript= {};
	</script>
	<meta name="_csrf_parameter" content="_csrf">
	<meta name="_csrf_header" content="X-CSRF-TOKEN">
	<meta name="_csrf"
		content="isGcfZeJe2G4cW4Key1n3GT-IuVztVbylCesAWtA96yF9XZ-6fT6S_W6HlKVFF9sSQBTuQCbD4dE0TTfoUbIOF4ikpixkEUc">
	<script type="module">const csrfParameterName = '_csrf';
const csrfCookieName = 'XSRF-TOKEN';
window.addEventListener('formdata', (e) => {
  if (!e.formData.has(csrfParameterName)) {
    return;
  }

  const cookies = new URLSearchParams(document.cookie.replace(/;\s*/, '&'));
  if (!cookies.has(csrfCookieName)) {
    return;
  }

  e.formData.set(csrfParameterName, cookies.get(csrfCookieName));
});
</script>
	<script initial="">
		window.Vaadin = window.Vaadin || {};
window.Vaadin.featureFlagsUpdaters = window.Vaadin.featureFlagsUpdaters || [];
window.Vaadin.featureFlagsUpdaters.push((activator) => {
});
	</script>
	<base href=".">
	<script type="text/javascript">
		window.JSCompiler_renameProperty = function(a) { return a;}
	</script>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
	<style>
		body,
		#outlet {
			height: 100vh;
			width: 100%;
			margin: 0;
		}
	</style>
	<!-- index.ts is included here automatically (either by the dev server or during the build) -->
	<script type="module" crossorigin src="./VAADIN/build/indexhtml-Dmatnos9.js"></script>
	<style>
		.v-reconnect-dialog,
		.v-system-error {
			position: absolute;
			color: black;
			background: white;
			top: 1em;
			right: 1em;
			border: 1px solid black;
			padding: 1em;
			z-index: 10000;
			max-width: calc(100vw - 4em);
			max-height: calc(100vh - 4em);
			overflow: auto;
		}

		.v-system-error {
			color: indianred;
			pointer-events: auto;
		}

		.v-system-error h3,
		.v-system-error b {
			color: red;
		}
	</style>
	<style>
		[hidden] {
			display: none !important;
		}
	</style>
	<!--CSSImport end-->
	<!--Stylesheet end-->
	<script initial="" src="./VAADIN/static/push/vaadinPush.js"></script>
	<script>
		const issuedWarnings = window.litIssuedWarnings ??= new Set();
issuedWarnings.add(
  'Multiple versions of Lit loaded. Loading multiple versions is not recommended. See https://lit.dev/msg/multiple-versions for more information.',
);
issuedWarnings.add(
  'Lit is in dev mode. Not recommended for production! See https://lit.dev/msg/dev-mode for more information.',
);
issuedWarnings.add(
  'Overriding ReactiveElement.createProperty() is deprecated. The override will not be called with standard decorators See https://lit.dev/msg/no-override-create-property for more information.',
);
issuedWarnings.add(
  'Overriding ReactiveElement.getPropertyDescriptor() is deprecated. The override will not be called with standard decorators See https://lit.dev/msg/no-override-get-property-descriptor for more information.',
);
	</script>
	<script>
		window.Vaadin = window.Vaadin ?? {};
window.Vaadin.views = {};
	</script>
</head>

<body>
	<!-- This outlet div is where the views are rendered -->
	<div id="outlet"></div>
	<script>
		window.Vaadin = window.Vaadin || {};
window.Vaadin.registrations = window.Vaadin.registrations || [];
window.Vaadin.registrations.push({"is":"flow/SpringInstantiator","version":"24.8.3"},{"is":"routing/server","version":"24.8.3"},{"is":"java","version":"23.0.2"},{"is":"SpringFramework","version":"6.2.8"},{"is":"has-flow-route","version":"24.8.3"},{"is":"flow/dev-bundle","version":"24.8.3"},{"is":"SpringBoot","version":"3.5.3"});
	</script>
	<vaadin-dev-tools></vaadin-dev-tools>
	<copilot-main></copilot-main>
</body>

</html>
2025-07-23T17:28:54.587+02:00 DEBUG 4376 --- [booking] [nio-8080-exec-6] o.s.security.web.FilterChainProxy        : Securing PUT /api/approval/approve/1e99fb3f-1e3a-43a7-b352-34cf51efdaab
2025-07-23T17:28:54.587+02:00 DEBUG 4376 --- [booking] [nio-8080-exec-6] o.s.s.w.a.AnonymousAuthenticationFilter  : Set SecurityContextHolder to anonymous SecurityContext
2025-07-23T17:28:54.588+02:00 DEBUG 4376 --- [booking] [nio-8080-exec-6] o.s.s.w.s.HttpSessionRequestCache        : Saved request http://localhost:8080/api/approval/approve/1e99fb3f-1e3a-43a7-b352-34cf51efdaab?continue to session
2025-07-23T17:28:54.588+02:00 DEBUG 4376 --- [booking] [nio-8080-exec-6] s.w.a.DelegatingAuthenticationEntryPoint : Trying to match using com.vaadin.flow.spring.security.VaadinWebSecurity$$Lambda/0x0000026a62d3fba8@297325dd
2025-07-23T17:28:54.588+02:00 DEBUG 4376 --- [booking] [nio-8080-exec-6] s.w.a.DelegatingAuthenticationEntryPoint : Trying to match using any request
2025-07-23T17:28:54.588+02:00 DEBUG 4376 --- [booking] [nio-8080-exec-6] s.w.a.DelegatingAuthenticationEntryPoint : Match found! Executing org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint@53899826
2025-07-23T17:28:54.588+02:00 DEBUG 4376 --- [booking] [nio-8080-exec-6] o.s.s.web.DefaultRedirectStrategy        : Redirecting to http://localhost:8080/login
2025-07-23T17:28:54.591+02:00 DEBUG 4376 --- [booking] [nio-8080-exec-7] o.s.security.web.FilterChainProxy        : Securing GET /login
2025-07-23T17:28:54.591+02:00 DEBUG 4376 --- [booking] [nio-8080-exec-7] o.s.s.w.a.AnonymousAuthenticationFilter  : Set SecurityContextHolder to anonymous SecurityContext
2025-07-23T17:28:54.591+02:00 DEBUG 4376 --- [booking] [nio-8080-exec-7] o.s.security.web.FilterChainProxy        : Secured GET /login

Check documentation here for vaadin.exclude-urls in your application.properties

This is needed when you have the REST API deployed on the same context as the Vaadin app itself. This configuration helps you exclude paths that are not to be handled by Vaadin’s Router.

You do not need to customize your index.html at all because of this.

Thank you for your answer. All my API endpoints are {host_name}/api/{endpoint_name}, and my vaadin views are {host_name}/{route_name}.
I have already set vaadin.exclude-urls=/api/* in my properties file. But i still get the same errors.

I am not sure if it is just a typo, but the pattern should end with two asterisks to work properly with your endpoints.
For example, the pattern you are using will match /api/approval but not /api/approval/approve/1e99fb3f-1e3a-43a7-b352-34cf51efdaab.

Try to use vaadin.exclude-urls=/api/**

Yeah sorry, that was a typo i have it with two asterisks.

You can take a look at this topic REST API next to Vaadin - #7 by OnDemand