I have a microservice architecture based project, for which I have added Spring Cloud Gateway in front. My plan is to centralize security in the gateway. I have added OIDC and OAuth 2 for authentication and authorization which work as expected. The gateway plays the role of OAuth client and resource server. It uses authorization code flow. I have used TokenRelay filter to pass the information to downstream services. I have also enabled CORS and CSRF (enabled by default) in the gateway.
The issue I am having is more related to CSRF than anything else. For regular http calls, everything seems to work just fine, as the downstream service checks for Authorization header (which is added via TokenRelay) and it seems to skip CSRF check, essentially trusting the gateway. I would like the same thing for websocket connections. Currently, if I enable websocket security (via @EnableWebsocketSecurity), it will enable CSRF check, but sending the gateway’s token will throw an exception, as it expects the downstream service’s token (it has its own token repository). If I disable websocket security (and CSRF implicitly), sending/not sending the gateway’s token makes no difference.
Is there a way to achieve the same behavior from http calls in websockets with gateway? I know one option would be to create some sort of centralized token repository and use it in both gateway and microservice, but it seems like a hassle.
My current solution is to disable CSRF for microservice, as I am not sure how CSRF token helps in case of websockets, as Spring Security/Spring Websockets implements server side Origin header check (server side same origin policy), which seems to be enough. I have already posted about this here.
LATER EDIT
For HTTP requests, the CsrfFilter will bypass csrf token check if there is a Bearer token present. This is due to the fact that the microservice is an oauth resource server (see explanation on OAuth2ResourceServerConfigurer.registerDefaultCsrfOverride). However this does not happen for websockets. Is this intended or is it a bug?
Below is the code:
Downstream service
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@EnableWebSocketSecurity
public class DownstreamServiceSecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
.csrf(csrfSpec -> csrfSpec.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
.oauth2ResourceServer((oauth2) -> oauth2.jwt(withDefaults()));
return http.build();
}
.....
Gateway
@Configuration
@EnableWebFluxSecurity
public class GatewaySecurityConfiguration {
@Autowired
public SecurityConfiguration(ReactiveClientRegistrationRepository clientRegistrationRepository) {
this.clientRegistrationRepository = clientRegistrationRepository;
}
ReactiveClientRegistrationRepository clientRegistrationRepository;
private ServerLogoutSuccessHandler serverLogoutSuccessHandler() {
OidcClientInitiatedServerLogoutSuccessHandler successHandler = new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository);
//needs to match the one declared at auth-server
successHandler.setPostLogoutRedirectUri("{baseUrl}/api-docs");
return successHandler;
}
@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http){
http.cors(withDefaults())
// add csrf token that can be handled by js clients by using CookieServerCsrfTokenRepository.withHttpOnlyFalse()
.csrf(csrfSpec -> csrfSpec.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new SpaServerCsrfTokenRequestHandler()))
.authorizeExchange(authorizeExchangeSpec -> authorizeExchangeSpec.pathMatchers("/api-docs/**", "/swagger-ui.html", "/webjars/swagger-ui/**", "/actuator/**", "/oidc/**").permitAll()
.pathMatchers("/notification/ws-connect").hasAuthority("SCOPE_customer-read")
.anyExchange().authenticated())
//handle oidc authentication by redirecting to auth server login page
.oauth2Login(withDefaults())
//make gateway also a resource server that supports jwt bearer token, as the default configuration does not kick in because we also have the client dependency on the classpath
.oauth2ResourceServer(oAuth2ResourceServerSpec -> oAuth2ResourceServerSpec.jwt(withDefaults()))
.logout(httpSecurityLogoutConfigurer -> httpSecurityLogoutConfigurer.logoutSuccessHandler(serverLogoutSuccessHandler()));
return http.build();
}
//When storing the expected CSRF token in a cookie, JavaScript applications will only have access to the plain token value and will not have access to the encoded value.
//A customized request handler for resolving the actual token value will need to be provided.
//See https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript-spa
static final class SpaServerCsrfTokenRequestHandler extends ServerCsrfTokenRequestAttributeHandler {
private final ServerCsrfTokenRequestAttributeHandler delegate = new XorServerCsrfTokenRequestAttributeHandler();
@Override
public void handle(ServerWebExchange exchange, Mono<CsrfToken> csrfToken) {
// Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of the CsrfToken when it is rendered in the response body.
this.delegate.handle(exchange, csrfToken);
}
@Override
public Mono<String> resolveCsrfTokenValue(ServerWebExchange exchange, CsrfToken csrfToken) {
final var hasHeader = exchange.getRequest().getHeaders().get(csrfToken.getHeaderName()) !=null;
return hasHeader ? super.resolveCsrfTokenValue(exchange, csrfToken) : this.delegate.resolveCsrfTokenValue(exchange, csrfToken);
}
}
@Bean
//Needed in order to set the XSRF-TOKEN cookie
//See https://docs.spring.io/spring-security/reference/reactive/integrations/cors.html
public WebFilter csrfCookieWebFilter() {
return (exchange, chain) -> {
exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty()).subscribe(o -> ((CsrfToken)o).getToken());
return chain.filter(exchange);
};
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("http://localhost:63342"));
configuration.setAllowedMethods(List.of(CorsConfiguration.ALL));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(List.of(CorsConfiguration.ALL));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Client
const stompClient = new StompJs.Client({
brokerURL: 'ws://localhost:9990/notification/ws-connect',
connectHeaders : {"X-XSRF-TOKEN":csrfToken}
});
Error when passing the token generated by gateway
org.springframework.security.web.csrf.InvalidCsrfTokenException: Invalid CSRF Token 'd1f38e4e-71cc-4241-8c7e-3b01c1937c7c' was found on the request parameter '_csrf' or header 'X-XSRF-TOKEN'
I have uploaded a diagram, maybe this explains the situation better:
Thanks in advance ! Any help is appreciated !