4

I am using Spring security within a Spring Boot Webflux application to serve traffic primarily on HTTPS port. However as an operational requirement I need to support couple of non-secure REST API paths in my Spring Boot application for health check etc that need to be exposed on HTTP as well.

So how do I enforce all the requests to HTTPS except for a known path using SecurityWebFilterChain bean?

This is how I have defined my SecurityWebFilterChain bean:

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {    
    @Bean
    SecurityWebFilterChain webFilterChain( ServerHttpSecurity http )
     throws Exception {
         return http 
            .authorizeExchange(exchanges -> exchanges
                    .anyExchange().permitAll()
                    .and()
                    .exceptionHandling()
                    .authenticationEntryPoint((exchange, exception) ->
                        Mono.error(exception))
                    )
            .csrf().disable()
            .headers().disable()
            .logout().disable()
            .build();
    }
}

This obviously won't work as intended because it is allowing all requests to use HTTP and HTTPS schemes whereas I want to always enforce HTTPS except for a path e.g. /health.

Please suggest what changes would I need in above code to get this done.

anubhava
  • 761,203
  • 64
  • 569
  • 643

2 Answers2

3

Here is what I came up with to to solve this problem. I am calling a custom matcher in .matchers( customMatcher ) method

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

    private static final Set<String> UNSECURED = 
                 Set.of ( "/health", "/heartbeat" );

    @Bean
    SecurityWebFilterChain webFilterChain( final ServerHttpSecurity http ) {    
        return http
                .authorizeExchange(
                        exchanges -> exchanges
                        .matchers( this::blockUnsecured ).permitAll()
                        .and()
                        .exceptionHandling()
                        .authenticationEntryPoint(
                               (exchange, exception) -> Mono.error(exception))
                        )
                .csrf().disable()
                .headers().disable()
                .logout().disable()
                .httpBasic().disable()
                .build();
    }

    Mono<MatchResult> blockUnsecured( final ServerWebExchange exchange ) {    
        // Deny all requests except few known ones using "http" scheme
        URI uri = exchange.getRequest().getURI();

        boolean invalid = "http".equalsIgnoreCase( uri.getScheme() ) &&
                !UNSECURED.contains ( uri.getPath().toLowerCase() );    
        return invalid ? MatchResult.notMatch() : MatchResult.match();    
    }
}

Not sure if there is a better way of doing same.

anubhava
  • 761,203
  • 64
  • 569
  • 643
  • I think my understanding of your question is totally incorrect. Correct me if I am wrong as I don't know webflux, if someone connect over https, they don't need to provide any login details, they can access the system. Is that correct? – Kavithakaran Kanapathippillai Jul 17 '20 at 14:22
  • Yes that is correct, there is no login as this application is using mutual TLS and clients are providing a valid key/cert pair to connect. – anubhava Jul 17 '20 at 14:23
  • In non webflux spring security, to use mutual tls ,there is `http.x509()`, since I don't see anything similar, verifying the client's tls configured at the server level and is not done by spring security? – Kavithakaran Kanapathippillai Jul 17 '20 at 14:30
  • If my understanding is correct, .ie `verifying the client certificate and doing mutual tls is not the responsibility of spring security in your case`, isn't overkill to bring the `spring security filter chain` here just to check if a request is http or https scheme as the chain doesn't do anything else? – Kavithakaran Kanapathippillai Jul 17 '20 at 14:42
  • That's correct I am not doing client cert verification in spring security. That is being done in `UndertowReactiveWebServerFactory` – anubhava Jul 17 '20 at 14:45
  • 1
    In my opinion, I will not at all use spring security for the purpose checking only some urls are allowed over http. I would use use simple `servlet filter` or `interceptor` ( i dont know what is it called in webflux but concentually plays the same role) and do the extact check your are doing in your custom matcher. It feels like buying car and dismantle it only to get the tyre – Kavithakaran Kanapathippillai Jul 17 '20 at 14:55
  • Fair point. I will try doing this in a servlet filter as well. Will it also allow disabling `csrf`, `headers` etc? – anubhava Jul 17 '20 at 15:01
  • 1
    When you stop using `@EnableWebFluxSecurity which autoenables csrf and httpBasic by default`, they wouldn't have been enabled anyway so you shouldn't have to disable them. – Kavithakaran Kanapathippillai Jul 17 '20 at 15:05
  • Hmm I wasn't aware of that. At least their [Javadoc](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurity.html) doesn't mention it. – anubhava Jul 17 '20 at 15:12
  • See the sentence `Below is the same as our minimal configuration, but explicitly declaring the ServerHttpSecurity.` in that page – Kavithakaran Kanapathippillai Jul 17 '20 at 15:53
  • Thanks for your helpful comments. I was able to achieve this in `WebFilter` as well. – anubhava Jul 17 '20 at 20:12
  • 1
    Great. Good for me too as I got introduced to web flux spring security – Kavithakaran Kanapathippillai Jul 17 '20 at 20:14
  • 1
    Although this might seem to work, there are few problems with this code. `.contains` is a bad replacement for `endsWith`. Someone might just pull his hairs off while debugging ;) Also, `getPath().toLowerCase` means `/abc` and `/ABC` would be treated as same URI. URI is case sensitive. As I suggested, you should just use the actuator and route the requests internally while keeping the whole website serving https scheme only. There is also a high impact on SEO ranking when you serve mixed content. – Concurrent Bhai Jul 18 '20 at 04:02
  • This is not an internet facing solution so SEO is not of any concern. Actuator is aisi out of bounds because of security reasons since it creates many endpoints and exposes lot of internal details. – anubhava Jul 18 '20 at 04:31
  • 1
    Also I don't understand `.contains is a bad replacement for endsWith`. There is no `.endsWith` in `Set` and even if it is there it won't serve the purpose. Also treating `/health` same as `/HALTH` is on purpose. – anubhava Jul 18 '20 at 06:23
  • May I ask why is this answer accepted? Is it working? I copy pasted the same block of code, and upon a curl into http health endpoint, I am still getting the io.netty.handler.ssl.NotSslRecordException: not an SSL/TLS record: I added couple of log around the two methods and both were invoked after the exception – PatPanda Sep 13 '20 at 09:52
  • Of course it is working. SSL handshake happens before this filter. Your `NotSslRecordException` means that only. – anubhava Sep 13 '20 at 09:54
0

Create a custom filter by copying HttpsRedirectWebFilter, and in that filter, if the requested url is other than /health, you will modify it such a way it sends 401 instead of redirect

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {    
    @Bean
    SecurityWebFilterChain springWebFilterChain( ServerHttpSecurity http )
     throws Exception {
         return http.addFilterAt(your-custom-https-filter, 
                                 SecurityWebFiltersOrder.HTTPS_REDIRECT)
                    .
                  ......
    }

    
  • Thanks for your answer. However [Javadoc of ServerHttpSecurity](https://github.com/spring-projects/spring-security/blob/master/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java) doesn't have `requiresChannel()` method. – anubhava Jul 16 '20 at 14:10
  • 1
    It is fine. feedback is feedback whatever form. There is a `.redirectToHttps()`, will it work if you used that one second filter? – Kavithakaran Kanapathippillai Jul 16 '20 at 14:18
  • Yes I have played with `.redirectToHttps()`. It does a `302` redirect which is not the desired behavior. I would prefer to send `500` or `401` back for unsecured access. – anubhava Jul 16 '20 at 14:28
  • So if it is enforces `https` but it does it with a redirect, Instead of using it ,you can create a custom filter by copying the code of `HttpsRedirectWebFilter` . In that custom filter if it is not https, instead of redirect, you can send 401 – Kavithakaran Kanapathippillai Jul 16 '20 at 14:36
  • Please share all the related code so that I can review it. – anubhava Jul 16 '20 at 14:44
  • See line 63 and 64, and that is where you want to change the behaviour instead of redirect. https://github.com/spring-projects/spring-security/blob/master/web/src/main/java/org/springframework/security/web/server/transport/HttpsRedirectWebFilter.java – Kavithakaran Kanapathippillai Jul 16 '20 at 14:55
  • And also `requiresHttpsRedirectMatcher` is initialised with `anyExchange()`, you can initialise with a new Matcher that excludes `/health` etc – Kavithakaran Kanapathippillai Jul 16 '20 at 15:19
  • I believe that is all used for https redirect and for that I can just use `redirectToHttps(Customizer httpsRedirectCustomizer)`. I have already tried it. It works but as I said it sends `302` – anubhava Jul 16 '20 at 15:29