CSRF necassary when having JWT stateless?

Hi everyone,

do i need to have CSRF when i use JWT stateless auth?

My understanding of it is, that of i have a JWT (which is generated/encrypted with a secret key), this JWT is added as a cookie and every request/navigation in the frontend the JWT is checked, if its missing or manipulated it will make a logout.

So a csrf isnt necassary because we are stateless or am i wrong?

my SecurityConfig looks like

Code SecurityConfig
@Configuration
public class SecurityConfigFrontend {

  private static final String[] PUBLIC_ENDPOINTS = {
      "/images/**", "/robots.txt", "/swagger-ui/**", "/v3/**",
      "/css/**", "/js/**", "/font-awesome/**", "/img/**",
      "/fonts/**", "/VAADIN/**", "/frontend/**",
      "/webjars/**", "/error", "/favicon.ico", "/?v-r=heartbeat*"
  };

  @Bean
  public SecurityFilterChain vaadinSecurityFilterChain(HttpSecurity http) throws Exception {

    http.securityMatcher(new NegatedRequestMatcher(
        new OrRequestMatcher(
            PathPatternRequestMatcher.withDefaults().matcher("/api/**"),
            PathPatternRequestMatcher.withDefaults().matcher("/webhooks/**"),
            PathPatternRequestMatcher.withDefaults().matcher("/basic-api/**")
        )
    ));

    http.authorizeHttpRequests(auth -> auth
        .requestMatchers(Arrays.stream(PUBLIC_ENDPOINTS)
          .map(p -> PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.GET, p))
          .toArray(PathPatternRequestMatcher[]::new)).permitAll()
        .requestMatchers("/", "/public/**").permitAll()
        .anyRequest().authenticated()
    );

    http.exceptionHandling(ex -> ex
        .accessDeniedHandler(new MyAccessDeniedHandler())
        .authenticationEntryPoint(new MyAuthenticationEntryPoint())
        .accessDeniedPage("/access-denied")
    );

    http.cors(withDefaults());
    http.addFilterBefore(new MyJwtFilter(), UsernamePasswordAuthenticationFilter.class);

    http.authenticationProvider(new MyAuthProvider());

    // JWT-Setup (Vaadin stateless)
    http.csrf(csrf -> csrf.disable());

    var secretKey = new SecretKeySpec(
        Base64.getDecoder().decode(System.getenv("JWT_SECRET_BASE64")),
        "HS256"
    );

    http.with(new VaadinStatelessSecurityConfigurer<>(), stateless -> stateless
        .issuer("example-issuer")
        .expiresIn(259_200)
        .withSecretKey()
        .secretKey(secretKey)
    );

    return http.build();
  }

CSRF is needed whenever cookies are used, regardless of whether the cookie contains a session id or a JWT.

CSRF protects against the fact that an attacker on a separate domain can make the browser send a request that includes the user’s cookie even though the browser’s cross-site restrictions prevent the attacker from reading the response.

Okay thanks, but how to handle it with vaadin? I have tried adding it and i see a XSRF but it makes no sense, if i remove or change the XSRF everything behave the same. So it doesnt matter if the XSRF token is there or not or do i await to much of it :S How can i confirm that it does what it should?

How should it be configured correctly?

The version with xsrf looks like:

CSRF Version of Security Config
@Bean
public SecurityFilterChain vaadinSecurityFilterChain(HttpSecurity http) throws Exception {

    // Frontend-Chain: alles auĂźer API/Webhooks/basic-api
    http.securityMatcher(new NegatedRequestMatcher(
        new OrRequestMatcher(
            PathPatternRequestMatcher.withDefaults().matcher("/api/**"),
            PathPatternRequestMatcher.withDefaults().matcher("/webhooks/**"),
            PathPatternRequestMatcher.withDefaults().matcher("/basic-api/**")
        )
    ));

    // Vaadin-Framework-Requests (UIDL/heartbeat/push) erkennen ĂĽber v-r Parameter
    final RequestMatcher vaadinInternal = request -> {
        final String vr = request.getParameter(
            com.vaadin.flow.shared.ApplicationConstants.REQUEST_TYPE_PARAMETER // "v-r"
        );
        return vr != null &&
               java.util.stream.Stream.of(com.vaadin.flow.server.HandlerHelper.RequestType.values())
                   .anyMatch(t -> t.getIdentifier().equals(vr));
    };

    http.authorizeHttpRequests(auth -> auth
        .requestMatchers(
            "/images/**", "/robots.txt", "/swagger-ui/**", "/v3/**",
            "/css/**", "/js/**", "/font-awesome/**", "/img/**",
            "/fonts/**", "/webjars/**", "/error", "/favicon.ico",
            "/VAADIN/**", "/frontend/**", "/themes/**"
        ).permitAll()
        .requestMatchers(vaadinInternal).permitAll()
        .requestMatchers("/", "/portal/**").permitAll()
        .anyRequest().authenticated()
    );

    http.exceptionHandling(ex -> ex
        .accessDeniedHandler(accessDeniedHandler)
        .authenticationEntryPoint(customAuthenticationEntryPoint)
        .accessDeniedPage("/access-denied")
    );

    // Authentifizierung stateless (JWT) – Vaadin Flow nutzt weiter HttpSession für UI-State
    http.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

    // CSRF: Cookie-Repo bereitstellen & als SharedObject registrieren
    CsrfTokenRepository csrfRepo = org.springframework.security.web.csrf.CookieCsrfTokenRepository.withHttpOnlyFalse();
    http.setSharedObject(org.springframework.security.web.csrf.CsrfTokenRepository.class, csrfRepo);
    http.csrf(csrf -> csrf
        .csrfTokenRepository(csrfRepo)
        .ignoringRequestMatchers(vaadinInternal) // optional
    );

    // Vaadin-Success-Handler als SharedObject (verhindert NPE in Configurer.init)
    var successHandler = new com.vaadin.flow.spring.security.VaadinSavedRequestAwareAuthenticationSuccessHandler();
    http.setSharedObject(com.vaadin.flow.spring.security.VaadinSavedRequestAwareAuthenticationSuccessHandler.class,
                         successHandler);

    http.authenticationProvider(customAuthenticationProvider());

    // Vaadin stateless + JWT (3 Tage), Secret aus Vault (HS256)
    javax.crypto.spec.SecretKeySpec secretKey = new javax.crypto.spec.SecretKeySpec(
        java.util.Base64.getDecoder().decode(vaultService.getSecret(KeyEnum.JWT_SECRET)),
        org.springframework.security.oauth2.jose.jws.JwsAlgorithms.HS256
    );

    http.with(new com.vaadin.flow.spring.security.stateless.VaadinStatelessSecurityConfigurer<>(), stateless -> stateless
        .issuer("example-issuer")
        .expiresIn(259_200) // 3 Tage
        .withSecretKey()
        .secretKey(secretKey)
    );

    // HTTPS only
    if (!environmentService.isLocalWindowsDevelopment()) {
        http.requiresChannel(channel -> channel.anyRequest().requiresSecure())
            .exceptionHandling(ex -> ex
                .accessDeniedHandler((req, res, e) -> {
                    res.setStatus(jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST                    res.setStatus(jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST);
                    res.getWriter().write("HTTPS required");
                })
            );
    }

    return http.build();

In the request payload i see a csrf token which is (automatically?) set by Vaadin/Spring?

Flow handles CSRF for you so no need to configure that separately with Spring Security for requests that are handled by Flow.

Could also be noted that there’s not really any point in using JWT for regular authentication with Flow since everything else with Flow is anyways stateful. If you get a JWT from an external system such as an OAuth authentication flow, then it’s usually easiest to verify that JWT once and then rely on the regular session for the rest of the session.

1 Like

ah so my first code is enough? Thats great. Does my Screenshot from my previous post shows the XSRF Token which generated Vaadin for me?

I need JWT because the users will be logged out otherway in the eventing, we redeploy which makes a restart of the service.

So the users sould be kept loggedin for x days (if they havent used the app for 3 days the jwt expires and are logged out)

Is there any other way? The JWT works pretty well

Yes, that csrfToken field in the shown JSON payload is the token that is automatically generated and verified by Vaadin.

Sounds like you’re using JWT as a more fancy “remember me” cookie. In that sense, the authentication in the session is the primary source and the JWT is just a fallback. To do things 100% properly with JWT, you still also need a separate mechanism for explicitly invalidating a JWT before it expires. That’s necessary to force logout for a user in case they change their password because their computer was compromised or things like that. And when you implement those things, then most of the benefits of JWT disappear and you could just as well use a simpler random id and store info about that id in the database.

Thanks, what do you mean with a simpler random id? Is there a example anywhere? The most important is, that the user must stay authenticated for x days. If not used he must be logged out. And it should survive a reboot, so the user still is logged in. All that JWT stateless is already working out of the box.

If i change the JWT i get logged out, excactly what i need. Sounds hart to build this by my own

https://docs.spring.io/spring-security/reference/servlet/authentication/rememberme.html

It doesn’t work in the opposite direction without additional infrastructure. If a user’s computer is compromised, then you need to prevent logging in with the token or cookie stored on that computer. This means that you need some state related to the functionality in the database.

1 Like