0

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:

enter image description here

Thanks in advance ! Any help is appreciated !

7
  • Have you tried to customize CSRF the same way (for a SPA) on both the gateway and the downstream service? Also, why do you feel you need CSRF protection on the downstream service? Commented Jun 28 at 14:11
  • My intention is to centralize security in gateway. However, gateway does not enforce CSRF protection for websockets, while the downstream service enforces it when @EnableWebSocketSecurity is used. That is why I have tried to use csrf in service. I think there is a mismatch between how spring gateway handles websocket security and how spring web does it, that is why I have created the bug. Also in theory, if you expose the downstream service to some backoffice UI, that calls the service directly, you could have a security issue if csrf is not enabled.
    – IonutB
    Commented Jun 28 at 17:24
  • For me the best CSRF behavior is the one that happens for regular http calls. Gateway checks first for csrf token and if valid send the request to downstream service. Downstream service checks to see if there is a bearer token and if found, skips checking for csrf token, else does the csrf check. This behavior does not happen for websockets, and I do not understand why is this different.
    – IonutB
    Commented Jun 28 at 17:29
  • I have already explained that websocket security is not affected by HTTP request security. That is why they are different. It seems you are actually intending to ask why the framework is not designed to automatically propagate configuration from HTTP requests to websockets. If that what you're asking? Commented Jun 28 at 19:44
  • @SteveRiesenberg I believe I have 2 questions actually : 1)How can we add csrf protection for websocket endpoints in the gateway 2)why aren't websockets subject to the same behavior like http calls (skip csrf check if bearer token is present) - this was the bug I have added on github. Also I think I found the reason why CsrfTokenRepository did not work. See my last comment from J Asgarov's response
    – IonutB
    Commented Jun 28 at 20:05

3 Answers 3

1

Thank you for providing a sample. It does clarify some things, but there are several aspects of your setup that require feedback and discussion.

First, I don't see your sample using bearer tokens to make requests to the gateway, therefore you can remove http.oauth2ResourceServer() configuration in the gateway.

Since you aren't using bearer tokens for requests from the client (I'm assuming it will actually be a single-page app?) to the gateway, CSRF protection is required for HTTP POST requests. CSRF protection in this case is between the client and the gateway, and has nothing to do with notification-service.

Second, the connect request is an HTTP GET, and does not require or support CSRF protection. However, the subsequent websocket handshake is between the client and the notification-service, with the gateway only proxying the request. The gateway's configuration regarding CSRF does not automatically impact this in any way. This is an odd setup because you are placing a websockets server in a resource server, and therefore requiring your client to have some knowledge of how your backend (behind the gateway) works. If you truly need to do this, you can make it transparent by matching CSRF configuration between the gateway (for HTTP requests) and the notification-service (for websocket messages), which you have done. Because this setup spans two systems, nothing will automatically match up the configuration for you.

Not sure is this was intended but is confusing. For http calls, Spring will skip CSRF validation if Bearer token is found, while for websockets it does not, even ig the initial connect message is a http call.

As I have mentioned, configuring HTTP requests to the resource server (e.g. GET /ws-connect) to require only a bearer token does not affect websocket messages in any way. This is not a bug. The two have separate configuration in Spring Security.

CSRF cookie and header do not match for websockets Connect endpoint, which was solved by using CsrfChannelInterceptor.

You have configured websockets with csrf protection on the resource server to use the CsrfChannelInterceptor (instead of XorCsrfChannelInterceptor) which effectively matches the configuration you use on the gateway with SpaServerCsrfTokenRequestHandler. This is correct, and is required because your SPA client is sending plain CSRF tokens instead of BREACH tokens (see CsrfTokenRequestAttributeHandler vs XorCsrfTokenRequestAttributeHandler).

The question on stackoverflow is "How to secure microservice websocket endpoint with Spring Cloud Gateway?". More specifically how to implement csrf for websocket connect endpoint in gateway. I have not found any way to do that. This example works, as I have added csrf for websockets in the downstream service, but this is not what I want.

The websocket connect request is for notification-service, not the gateway. It's not clear to me why you expect anything different here. Regardless, you cannot disable CSRF protection on the gateway or the websockets server because requests are coming from a browser. Since you are only proxying requests and messages, the gateway cannot improve the situation for you.

All in all, your configuration seems mostly correct to me. It is however an advanced setup. We could potentially improve documentation (that's always a good option), but this setup is fairly specific so I'm not sure what that would look like.

3
  • thank you very much for the response. I have added an answer as it was to long for a comment
    – IonutB
    Commented Jul 8 at 12:46
  • You said "Since you are only proxying requests and messages, the gateway cannot improve the situation for you". This means that if I would implement websockets at gateway level, which in turn call notification-service, would this solve my problem?
    – IonutB
    Commented Jul 9 at 6:45
  • @SteveRisenBerg sorry wasn’t sure how to reach you. I have a Spring Security issue. Wondered if you’re able to help at all: stackoverflow.com/questions/78747321/…
    – Sachin
    Commented Jul 15 at 8:15
1

You can define to use CookieServerCsrfTokenRepository which means CSRF tokens won't be persisted on the Server and instead will be persisted in Cookies only:

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
    .csrf(csrf -> csrf.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()))
    .build();
}

Then the front-end has to read the value from Cookie (XSRF-TOKEN) and send it with header X-XSRF-TOKEN. This would solve your problem. Backend will compare both and allow request if both are present and have matching values.

See Using the CookieCsrfTokenRepository

10
  • Thank you for the response. Unfortunately this does not solve the issue. I was already using CookieServerCsrfTokenRepository. Maybe I did not describe the issue correctly.. If I enable CSRF on the downstream service for websockets(via @EnableWebSocketSecurity), it will ask for a token, but the one from gateway is of no use, as it checks the gateway token against the downstream service token repository, and of course, it throws invalid token exception. CookieServerCsrfTokenRepository helps for allowing a request to be accepted by the gateway, but not by the downstream service websockets
    – IonutB
    Commented Jun 26 at 14:41
  • 1
    @IonutB Are you passing both the Cookie (e.g. XSRF-TOKEN) and the header (e.g. X-XSRF-TOKEN) to the downstream service, as well as configuring the downstream service to use cookie-based CsrfTokenRepository? You have not shared any code, so it is difficult to know what your configuration looks like for each application. Commented Jun 27 at 22:49
  • @Steve Initially I did not have CsrfTokenRepository on the downstream service, only on gateway. I added it and the behavior is the same. I am sending both the header and cookie, but the token is generated by the gateway, so of course it will try to find it in the downstream service repository, hence InvalidCsrfTokenException. With the same setup, if I make a REST call, everything works as expected because CsrfFilter checks for Bearer token and skips CSRF token check. I was expecting this logic for websocket calls also, that is why I created a bug. I will try to post some code here today.
    – IonutB
    Commented Jun 28 at 6:17
  • 1
    with CookieServerCsrfTokenRepository it doesn't try to find it in memory, but only checks if cookie and header values match (stateless). So if u were to setup both with that then you shouldn't have the problem.
    – J Asgarov
    Commented Jun 28 at 6:31
  • For http calls it does not even try to match, it just skips CSRF check, logging : "Did not protect against CSRF since request did not match And [CsrfNotRequired [TRACE, HEAD, GET, OPTIONS], Not [Or [org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer$BearerTokenRequestMatcher@1f05d08c]]]".
    – IonutB
    Commented Jun 28 at 7:02
0

I believe that in order to understand my answers, you need to understand the architecture I am trying to achieve, as this of course will impact how I think and what I expect (wrongly or not) from the framework itself. My intention is to have a bunch of microservices that are accessed via a gateway. From my knowledge, one of the things you implement in the gateway is security. CSRF is part of security so this should be implemented in the gateway too. The practice that I am aware of is to centralize security in the gateway, in order to have a single door to guard. I am expecting to be able to guard any endpoint, regardless if it is http or websocket. Based on this rationale here are my answers:

First, I don't see your sample using bearer tokens to make requests to the gateway, therefore you can remove http.oauth2ResourceServer() configuration in the gateway.

Please ignore as I will have an SPA with the gateway as the OAuth client. Hopefully, having the gateway as a resource server is not the root cause of my issue here. I will remove it and see what happens.

Since you aren't using bearer tokens for requests from the client (I'm assuming it will actually be a single-page app?) to the gateway, CSRF protection is required for HTTP POST requests. CSRF protection in this case is between the client and the gateway, and has nothing to do with notification-service.

I understand that CSRF is required for POST requests, but since notification-service is using spring-websockets, which requires CSRF token for websocket endpoint (https://docs.spring.io/spring-security/reference/servlet/integrations/websocket.html#websocket-sameorigin-csrf), shouldn't the gateway be also aware and involved in this?

As I have mentioned, configuring HTTP requests to the resource server (e.g. GET /ws-connect) to require only a bearer token does not affect websocket messages in any way. This is not a bug. The two have separate configuration in Spring Security.

Not sure I understand this one, since spring-security checks by default if there is a bearer token, and if one is found, it skips csrf token validation, as it considers that there cannot be a csrf attack if you have a token instead of session cookie. IMHO this should be the case for websockets also, as a csrf attack happens in the same fashion as http. Check this for an explanation on bearer token-csrf behavior for http

You have configured websockets with csrf protection on the resource server to use the CsrfChannelInterceptor (instead of XorCsrfChannelInterceptor) which effectively matches the configuration you use on the gateway with SpaServerCsrfTokenRequestHandler. This is correct, and is required because your SPA client is sending plain CSRF tokens instead of BREACH tokens (see CsrfTokenRequestAttributeHandler vs XorCsrfTokenRequestAttributeHandler).

Not sure what I am doing wrong here. The SpaServerCsrfTokenRequestHandler uses XorCsrfTokenRequestAttributeHandler as a delegate. By default, XorCsrfChannelInterceptor is used on the notification-server side. Shouldn't XorCsrfTokenRequestAttributeHandler work with XorCsrfChannelInterceptor? While debugging I saw 2 identical strings not being equal...

The websocket connect request is for notification-service, not the gateway. It's not clear to me why you expect anything different here.

Based on my rationale, the websocket connect request is for the gateway, as the SPA has no knowledge of any notfication-service, nor it should have. The whole point of the gateway is to be the entry point in the system and to implement routing, circuit-breaking and security to name a few. So gateway should receive the request, apply security(check for csrf token, etc) and route to notification-service if the request passes security checks.

All in all, your configuration seems mostly correct to me. It is however an advanced setup. We could potentially improve documentation (that's always a good option), but this setup is fairly specific so I'm not sure what that would look like.

I am not sure why this is an advanced/specific setup, as having security in the gateway is quite standard IMHO. Because of the websockets situation I am forced to have security configuration scattered around services and gateway, which does not seem like a good design. Gateway should be able to protect all type of endpoints, regardless of the endpoint type (http,websockets or anything else).

Not the answer you're looking for? Browse other questions tagged or ask your own question.