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.
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:
- 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();
}
- 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 {}