Home > Back-end >  Spring Security not sending CSRF token in REST Application
Spring Security not sending CSRF token in REST Application

Time:12-16

I'm new to Spring Security and I'm trying to understand the CSRF mechanism. I have a Spring based application with Angular. As far as I know, Spring will send a CSRF Token in a cookie on the first GET Request it recieves (called XSRF-TOKEN) and then on every subsequent request it will look for that token in another cookie called (X-XSRF-TOKEN). My problem is that it doesn't generate a token and I can't figure out why. I'm using the latest Spring and Angular versions.

Link to the github repo

I created a dummy application, in which I have a /home endpoint, which accepts GET request and then returns a dummy string. I've set the withCredentials: true on the Angular side and I've configured CookieCsrfTokenRepository. I don't see the Set-cookie header containing the token. What am I expected to do and what am I doing wrong?

Spring Security Configuration

@Configuration
@EnableWebSecurity
public class SecurityConfiguration {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .cors()
                .and()
                .csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeHttpRequests()
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations())
                .permitAll()
                .requestMatchers("/home")
                .permitAll()
                .anyRequest()
                .authenticated();

        return http.build();
    }

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowCredentials(true);
        corsConfiguration.setAllowedOrigins(Collections.singletonList("http://localhost:4200"));
        corsConfiguration.setAllowedHeaders(Arrays.asList("Origin", "Access-Control-Allow-Origin", "Content-Type",
                "Accept", "Authorization", "Origin, Accept", "X-Request-With", "Access-Control-Request-Method",
                "Access-Control-Request-Headers", "XSRF-TOKEN", "X-XSRF-TOKEN"));
        corsConfiguration.setExposedHeaders(Arrays.asList("Origin", "Content-Type", "Accept", "Authorization",
                "Access-Control-Allow-Origin", "Access-Control-Allow-Credentials", "XSRF-TOKEN", "X-XSRF-TOKEN"));
        corsConfiguration.setAllowedMethods(Arrays.asList(
                HttpMethod.GET.name(), HttpMethod.POST.name(),
                HttpMethod.PUT.name(), HttpMethod.PATCH.name(),
                HttpMethod.DELETE.name(), HttpMethod.OPTIONS.name()
        ));
        urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
        return new org.springframework.web.filter.CorsFilter(urlBasedCorsConfigurationSource);
    }
}

Home Controller

@RestController
public class HomeResource {

    @GetMapping("/home")
    public String home() {
        return "Home is working";
    }
}

Angular

home.service.ts

@Injectable({
  providedIn: 'root'
})
export class HomeService {

  apiHost: string = 'http://www.localhost:8000';

  constructor(private http: HttpClient) {

  }

  public home(): Observable<string> {
    return this.http.get(`${this.apiHost}/home`, {withCredentials: true, responseType: 'text'});
  }
}

home.component.html

<p>{{home$ | async}}</p>

home.component.ts

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {

  home$!: Observable<string>;

  constructor(private homeService: HomeService) {
  }

  ngOnInit(): void {
    this.home$ = this.homeService.home();
  }
}

CodePudding user response:

Please refer to this Migration documentation, and read it completely.

In summary, Spring Security 6.0 defer the loading of the CsrfToken, this means that it will not include the token automatically in the response.

You have a few ways to solve this:

  1. Restore the old behavior (as described in the documentation):
@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
    CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
    // set the name of the attribute the CsrfToken will be populated on
    requestHandler.setCsrfRequestAttributeName(null);
    http
        // ...
        .csrf((csrf) -> csrf
            .csrfTokenRequestHandler(requestHandler)
        );
    return http.build();
}
  1. Create a controller endpoint that retrieves the CsrfToken from the request:
@RestController
@RequestMapping("/csrf")
public class CsrfController {

    @GetMapping
    public void getCsrfToken(HttpServletRequest request) {
        // https://github.com/spring-projects/spring-security/issues/12094#issuecomment-1294150717
        CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
        csrfToken.getToken();
    }

}

You might consider http.requestMatchers("/csrf").permitAll().

In all cases, you have simply to access the CsrfToken from request attributes.

And, on the Angular side, you can create a APP_INITIALIZER provider that will get the CSRF token when the application initializes:

function getCsrfToken(httpClient: HttpClient): () => Observable<any> {
  return () => httpClient.get('/csrf').pipe(catchError((err) => of(null)));
}

@NgModule({
  ...
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: getCsrfToken,
      deps: [HttpClient],
      multi: true,
    },
  ],
  ...
})
export class AppModule {}
  • Related