CSRF handling: changes from 24.7 to 24.8?

Hi!
Has something changed in the CSRF handling from 24.7 to 24.8? I do not really need it since I use stateless JWT authentication for my own endpoints but since I updated from 24.7.5 to 24.8.0.beta2 I get problems after logging in into my app, apparently because the renewed CSRF token does not make it into the browser and subsequent calls to /connect/** fail. Refreshing the page after login helps, but for now I probably have to disable CSRF for /connect/**

Thanks in advance
Olaf

[nio-8183-exec-6] o.s.s.w.savedrequest.CookieRequestCache  : saved request doesn't match
[nio-8183-exec-6] o.s.s.w.a.AnonymousAuthenticationFilter  : Set SecurityContextHolder to anonymous SecurityContext
[nio-8183-exec-6] o.s.security.web.FilterChainProxy        : Secured GET /VAADIN/push?v-r=push&debug_window&token=ad41cd95-46d5-4d3b-acbd-3b484c19f17f&X-Atmosphere-tracking-id=0&....
[nio-8183-exec-4] o.s.security.web.FilterChainProxy        : Securing POST /connect/UiSettingsService/fetchUiSettings
[nio-8183-exec-5] o.s.w.r.f.client.ExchangeFunctions       : [5e643950] HTTP GET http://localhost:8130/internal/v1/subscriptions/....
[r-http-epoll-33] o.s.w.r.f.client.ExchangeFunctions       : [5e643950] [07da98fc-2] Response 200 OK
[r-http-epoll-33] org.springframework.web.HttpLogging      : [5e643950] [07da98fc-2] Decoded [ (truncated)...]
[nio-8183-exec-5] o.s.w.r.f.client.ExchangeFunctions       : [19735b97] HTTP GET http://localhost:8130/internal/v1/users/...
[or-http-epoll-9] o.s.w.r.f.client.ExchangeFunctions       : [19735b97] [c4c32d30-61] Response 200 OK
[or-http-epoll-9] org.springframework.web.HttpLogging      : [19735b97] [c4c32d30-61] Decoded [User(authProviderId=firebase, (truncated)...]
[nio-8183-exec-5] o.s.s.w.savedrequest.CookieRequestCache  : saved request doesn't match
[nio-8183-exec-5] .s.ChangeSessionIdAuthenticationStrategy : Changed session id from FB9F373CD166885EF1A71EBE9C153699
[nio-8183-exec-5] o.s.s.w.csrf.CsrfAuthenticationStrategy  : Replaced CSRF Token
[nio-8183-exec-4] o.s.s.w.savedrequest.CookieRequestCache  : saved request doesn't match
[nio-8183-exec-4] o.s.s.w.csrf.CsrfAuthenticationStrategy  : Replaced CSRF Token
[...]
[nio-8183-exec-2] o.s.security.web.FilterChainProxy        : Securing POST /connect/UiSettingsService/getAvailableLanguages
[nio-8183-exec-2] o.s.security.web.csrf.CsrfFilter         : Invalid CSRF token found for http://localhost:8183/connect/UiSettingsService/getAvailableLanguages
[nio-8183-exec-3] o.s.security.web.FilterChainProxy        : Securing POST /connect/UiSettingsService/getAvailableCountries
[nio-8183-exec-3] o.s.security.web.csrf.CsrfFilter         : Invalid CSRF token found for http://localhost:8183/connect/UiSettingsService/getAvailableCountries
[io-8183-exec-10] o.s.security.web.FilterChainProxy        : Securing POST /connect/CurrencyService/getAvailableCurrencies
[io-8183-exec-10] o.s.security.web.csrf.CsrfFilter         : Invalid CSRF token found for http://localhost:8183/connect/CurrencyService/getAvailableCurrencies
[nio-8183-exec-8] o.s.security.web.FilterChainProxy        : Securing POST /connect/UiSettingsService/getAvailableTimeZones
[nio-8183-exec-8] o.s.security.web.csrf.CsrfFilter         : Invalid CSRF token found for http://localhost:8183/connect/UiSettingsService/getAvailableTimeZones

Hi, thanks for the report.

Is it so that your application updates CSRF meta tags in the document head dynamically after login?

The CSRF implementation in 24.8 is not using update automatically. I think the best we could do here is provide an API to reset the stored value in the Hilla client using meta tags. Would it help?

I do not touch any meta tags. I have a Middleware which sets the JWT in the Bearer authorization. That’s it.

An API would definitely help because I could easily add a CSRF Middleware to the ConnectClient. Another thing is that the CSRF apparently returns a 401 instead of a 403 so that it would be difficult to distinguish CSRF from real 401. On the other hand, I do not really know where this 401 comes from.

Update: Or is the 401 is correct in this situation? It’s just my case with authentication separated from the CSRF protection so that I expected a 403. But from the point of view of the Hilla app a 401 seems to be correct.

POST http://localhost:8183/connect/UiSettingsService/getAvailableCountries 401 (Unauthorized)
copilotMiddleware	@	vaadin.ts:44
BearerMiddleware	@	bearer-middleware.ts:45
await in call		
getAvailableCountries_1	@	UiSettingsService.ts:8

401 for blocked CSRF sounds more right to me - the request was denied because it couldn’t be authenticated. 403 would be “we have authenticated you and you don’t have rights to access this resource”.

Thanks for the clarification. I was thinking that rather than trying to handle CSRF error response, the error could be prevented if Hilla would handle the session change during login.

Naturally if the page is reloaded after login, the new token is obtained through index.html request, and the session change is handled this wa. But from the information so far I assume that the login process in your application does not produce a page reload, and thus you have an issue.

Given that you don’t touch meta tags, I guess that Spring Security configured to use CookieCsrfTokenRepository or similar? If so, Hilla client could probably support it and use the up-to-date token automatically from the cookies.

However, if JWT Bearer authorization is required for endpoints, then CSRF protection for Hilla endpoints is indeed unnecessary. So disabling CSRF for Hilla endpoints also makes sense as an alternative.

No, I have a simple SecurityConfig with just an additional filter verifying the JWT. No cookies, no nothing.

I played an hour with a CSRF endpoint and a Middleware to update the header and the _csrf meta but to no avail. So, I disable CSRF for now.

Thanks for your reply.
Olaf

I found the following code in 24.5.7 → @vaadin/hilla-frontend/Connect.js → ConnectClient#call

const csrfHeaders = getCsrfTokenHeadersForEndpointRequest(document);
const headers = {
  Accept: "application/json",
  "Content-Type": "application/json",
  ...csrfHeaders
};

Does this exist in 24.8?
Is the csrf token send on each endpoint request, visible in Chrome Dev Tools?

I am also running into CSRF issues after upgrading to 24.8. I am not touching CSRF at all.

I use stateless authentication as described in the Guide. The /login works, but the subsequent call that is defined in auth.ts (How to add a Hilla login view to a Vaadin application)

const auth = configureAuth(...);

returns a 401. The Debug logs point to a CSRF issue:

Invalid CSRF token found for ...

This is coming from org.springframework.security.web.csrf.CsrfFilter. When I add a breakpoint, I see that the supplied csrf token has a completely different format than the expected one:

Actual token in X-XSRF-TOKEN:

sXArJKAJxYWnUTe0_V4kVsO-9zUjvdCF2QtfddNNpAQVv3ME10FJFMFtoLWKNASHynMQZKbc2gxG3-io7G5tFLYukGFwiEE1

Expected

a1ac3628-dbdc-4254-b66c-49e962ed6e2e

Looking at the network traffic, the response seems to always include an X-XSRF-TOKEN in the correct format, but the request contains the other longer format.

i tried seeing the changes introduced in 24.8. in that part of the code, but am not sure if I was able to pin-point it.

In hilla/packages/ts/frontend/src/Authentication.ts at main · vaadin/hilla · GitHub you can see it doesn’t write the value from the X-XSRF-TOKEN into the <meta>-tag, but the Spring-CSRF-token Header.

Is anyone else seeing this issue with 24.8?

Some network calls copied from the dev console:

/login Request

Summary
URL: http://localhost:8080/login
Status: 200
Source: Network
Address: ::1:8080
Initiator:
Authentication.ts:199

Request
POST /login HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,en;q=0.9
Cache-Control: no-cache
Connection: keep-alive
Content-Length: 256
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryIWASd2V5J4nx1r9o
Cookie: JSESSIONID=8E771314C2E53C3B7E6B6ECB5AB1CD43; REDIRECT_URI=aHR0cDovL2xvY2FsaG9zdDo4MDgwLw==; XSRF-TOKEN=5c472ed2-1ebb-46a1-aaf5-5151db6cf845
Host: localhost:8080
Origin: http://localhost:8080
Pragma: no-cache
Referer: http://localhost:8080/login
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
source: typescript
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.4 Safari/605.1.15
x-xsrf-token: 5c472ed2-1ebb-46a1-aaf5-5151db6cf845

Response
HTTP/1.1 200
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: keep-alive
Content-Length: 0
Date: Wed, 25 Jun 2025 20:41:10 GMT
Default-url: /
Expires: 0
Keep-Alive: timeout=60
Pragma: no-cache
Result: success
Set-Cookie: JSESSIONID=203CE4C3CCD283765CB967C909B6CCF6; Path=/; HttpOnly
Set-Cookie: XSRF-TOKEN=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:10 GMT; Path=/
Set-Cookie: jwt.headerAndPayload=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJkZS5hdmVudG8uZGFzaGJvYXJkIiwic3ViIjoiYWRtaW5AZXhhbXBsZS5jb20iLCJleHAiOjE3NTA5MjAwNzAsImlhdCI6MTc1MDg4NDA3MCwicm9sZXMiOlsiQURNSU4iLCJBRE1JTl9DUkVBVEVfVVNFUiIsIlVTRVIiXX0; Max-Age=35999; Expires=Thu, 26 Jun 2025 06:41:09 GMT; Path=/
Set-Cookie: jwt.signature=Ee0Ug3utvoiaVRamkMZXM36C8cNVSbTP5gHaBsYEUx0; Max-Age=35999; Expires=Thu, 26 Jun 2025 06:41:09 GMT; Path=/; HttpOnly
Set-Cookie: XSRF-TOKEN=7165b48c-53cd-439d-891a-133848669e10; Path=/
Spring-CSRF-header: X-XSRF-TOKEN
Spring-CSRF-token: 54zFNRTXCD1XnYoATHRvzOULWYeV_303PzAN_h_5A8FkfDRd0u_xAiaybA96rO9iLllb-oQ6dOb0mUgaCgE4z3ubNaICRABo
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 0

Subsequent request

ummary
URL: http://localhost:8080/connect/UserEndpoint/get
Status: —
Source: —
Initiator:
chunk-PEOKBDED.js:906

Request
Accept: application/json
Cache-Control: no-cache
Content-Type: application/json
Origin: http://localhost:8080
Pragma: no-cache
Referer: http://localhost:8080/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.4 Safari/605.1.15
X-XSRF-TOKEN: j5BRT0Tb2S_05XFcUW4mVb395RjmYD_G8iaIowPzJQQmLo8Uu_Nkf3K96kzZgRdlM0MSbNyfyHnQAwfrlBbqwDvCQDYeTb1w

@david.bellem
I tried to reproduce this by starting a very simple Hilla application (from scratch) and adding security and login following the same guide. Following the steps, I could see the 401 and that longish token when calling the getUserInfo.

Then, to be sure that this is a problem in 24.8, I downgraded this test app to 24.7.7, and saw the same was happening in 24.7.

So I started looking and realized I was using the credentials of user/password from the guide (now it is fixed in the docs), while the actual credentials set up in the SecurityConfig.java (from the sample code in the guide) is user/user, and by using the correct credentials it works in 24.8 as well as in 24.7.

Given that you were following How to add a Hilla login view to a Vaadin application I assume that you’re not using a stateless security as this thread was originally about, but can confirm that using the correct credentials, it is working in the 24.8. But, please feel free to open an issue if you see the problem persists / or I’m missing something here.

2 Likes

I am using stateless as described here

I already have the app running with different username and password (stored in DB). In 24.7 and 24.8.0.alpha7 it worked fine and in 24.8. it only works after adding

http.csrf(httpSecurityCsrfConfigurer -> httpSecurityCsrfConfigurer.disable());

to the SecurityConfiguration.java

1 Like

Alright, now it makes sense. Disabling the CSRF check is completely fine for stateless security. We should probably dig into what has changed that now user code needs to manually do that. Good catch!

I think, that is only true if you use Headers to send the JWT information. If you store it in cookies, then you are still vulnerable to CSRF. If you look at the requests above, the server actually instructs to set cookies:

Set-Cookie: jwt.headerAndPayload=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJkZS5hdmVudG8uZGFzaGJvYXJkIiwic3ViIjoiYWRtaW5AZXhhbXBsZS5jb20iLCJleHAiOjE3NTA5MjAwNzAsImlhdCI6MTc1MDg4NDA3MCwicm9sZXMiOlsiQURNSU4iLCJBRE1JTl9DUkVBVEVfVVNFUiIsIlVTRVIiXX0; Max-Age=35999; Expires=Thu, 26 Jun 2025 06:41:09 GMT; Path=/
Set-Cookie: jwt.signature=Ee0Ug3utvoiaVRamkMZXM36C8cNVSbTP5gHaBsYEUx0; Max-Age=35999; Expires=Thu, 26 Jun 2025 06:41:09 GMT; Path=/; HttpOnly

Looking in the dev console, they are also saved:

And in VaadinStatelessSecurityConfigurer.java it mentions that JWT is saved in a cookie and it is very explicit about setting CSRF.

That is why I think, I need CSRF and should not just disable it, but would be happy to hear what others think.

Thanks for your help!

True indeed, as in this case the JWT is set in cookies, the CSRF attack can happen and CSRF filter should be enabled. It is only safe to disable the CSRF when the JWT token is sent via Authorization header which is not included automatically by the browser in cross-origin requests.

I’m experiencing the exact same problem described above after upgrading to 24.8 with stateless authentication. Login works fine, but subsequent Hilla service calls are failing with 401 errors.

What’s the recommended solution for this in a Hilla app with stateless security? Wait for a fix to be released and roll back for now? Is there a quick workaround we could use?

Created an issue for this: [24.8] CSRF token handling is broken in Stateless Security · Issue #3697 · vaadin/hilla · GitHub

3 Likes