Describe the bug

When adding the H2 console as an exception (white listing) in the SecurityFilterChain, the /h2-console returns a 401. This issue has occurred after migrating to Spring Boot 3 and changing antMatchers to requestMatchers.

To Reproduce

Full SecurityFilterChain:

@Bean
    public SecurityFilterChain filterChain(HttpSecurity http, ServerProperties serverProperties)
            throws Exception {

        // Enable OAuth2 with custom authorities mapping
        http.oauth2ResourceServer()
                .jwt()
                .jwtAuthenticationConverter(customJwtAuthenticationConverter(roleService)).and()
                // Using a custom handler for access denied exceptions
                .accessDeniedHandler(accessDeniedHandler())
                // Using a delegated authentication entry point to forward to controller advice
                .authenticationEntryPoint(authEntryPoint);

        // Enable anonymous
        http.anonymous();

        // Enable and configure CORS
        http.cors().configurationSource(corsConfigurationSource());

        // State-less session (state in access-token only)
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        // Enable CSRF with cookie repo because of state-less session-management
        http.csrf().disable();

        // If SSL enabled, disable http (https only)
        if (serverProperties.getSsl() != null && serverProperties.getSsl().isEnabled()) {
            http.requiresChannel().anyRequest().requiresSecure();
        } else {
            http.requiresChannel().anyRequest().requiresInsecure();
        }

        // Route security: authenticated to all routes but Swagger-UI
        // @formatter:off
        http.authorizeHttpRequests((authorize) -> authorize
                .requestMatchers( "/h2-console/**").permitAll()
                .requestMatchers( "/v3/api-docs/**").permitAll()
                .requestMatchers( "/swagger-ui/**").permitAll()
                .requestMatchers("/**").hasAnyRole("admin", "user"));
        // @formatter:on

        return http.build();
    }

    private CorsConfigurationSource corsConfigurationSource() {
        // Very permissive CORS config...
        final var configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(List.of("*"));
        configuration.setAllowedMethods(List.of("*"));
        configuration.setAllowedHeaders(List.of("*"));
        configuration.setExposedHeaders(List.of("*"));

        // Limited to API routes (neither actuator nor Swagger-UI)
        final var source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);

        return source;
    }

This is on a oauth2-resource-server, authenticating to Keycloak. This works fine for Swagger, with 'http://localhost:8081/swagger-ui/index.html' fully accessible. Swagger is using implementation org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.0.

Expected behavior

Should be able to access /h2-console

Sample

https://github.com/sfoxall/resource-server-keycloak

Comment From: marcusdacoregio

Hi @sfoxall, can you provide a sample using minimal configurations? This way we will be able to isolate the problem more efficiently without dealing with stuff like OAuth2, CORS, etc

Comment From: sfoxall

@marcusdacoregio no problem, will sort this shortly. Whilst diving into some of the notes, I have found a workaround:

https://github.com/spring-projects/spring-security/pull/12137/files#diff-dca6e5f09c8f1567d70a22e8d9a878e538a5c1c020aed13b38473f58dc40fd96

This works fine:

http.authorizeHttpRequests((authorize) -> authorize
         .requestMatchers(antMatcher("/h2-console/**")).permitAll()
         .requestMatchers(antMatcher("/v3/api-docs/**")).permitAll()
         .requestMatchers(antMatcher( "/swagger-ui/**")).permitAll()
         .requestMatchers(antMatcher("/**")).hasAnyRole("admin", "user"));

Which suggests there is a different behaviour for requestMatchers and antMatcher.

Comment From: sfoxall

@marcusdacoregio Hopefully this is better :-):

https://github.com/sfoxall/H2RequestMatcher

Config:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, ServerProperties serverProperties)
            throws Exception {

        // Enable anonymous
        http.anonymous();

        // Disable CORS
        http.cors().disable();

        // Route security: deny all routes accept Swagger-UI and H2-Console. But /h2-console returns a 403
        http.authorizeHttpRequests((authorize) -> authorize
                .requestMatchers( "/h2-console/**").permitAll()
                .requestMatchers( "/v3/api-docs/**").permitAll()
                .requestMatchers( "/swagger-ui/**").permitAll()
                .requestMatchers("/**").denyAll());

        // This configuration works fine
//        http.authorizeHttpRequests((authorize) -> authorize
//                .requestMatchers(antMatcher( "/h2-console/**")).permitAll()
//                .requestMatchers(antMatcher( "/v3/api-docs/**")).permitAll()
//                .requestMatchers(antMatcher( "/swagger-ui/**")).permitAll()
//                .requestMatchers(antMatcher("/**")).denyAll());

        return http.build();
    }
}

Comment From: marcusdacoregio

Thanks @sfoxall,

are you trying to perform a GET request to /h2-console or is it to another path?

Comment From: sfoxall

@marcusdacoregio Yes, that's right:

http://localhost:8082/h2-console

returns a 403.

If you change the config to:

http.authorizeHttpRequests((authorize) -> authorize
                .requestMatchers(antMatcher( "/h2-console/**")).permitAll()
               .requestMatchers(antMatcher( "/v3/api-docs/**")).permitAll()
                .requestMatchers(antMatcher( "/swagger-ui/**")).permitAll()
                .requestMatchers(antMatcher("/**")).denyAll());

then the console is returned.

Comment From: marcusdacoregio

Hi @sfoxall, I was able to figure out what is happening.

The H2ConsoleAutoConfiguration will register a Servlet for H2's Web Console, therefore, the servletPath property is needed in order to use the MvcRequestMatcher, like so:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) {
    // ...
    MvcRequestMatcher h2RequestMatcher = new MvcRequestMatcher(introspector, "/**");
    h2RequestMatcher.setServletPath("/h2-console");
    http.authorizeHttpRequests((authorize) -> authorize
        .requestMatchers(h2RequestMatcher).permitAll()
        // ...
    );
}

In summary, we are permitting every (/**) request under the h2-console servlet path.

Another option is to use PathRequest.toH2Console() as shown in the Spring Boot H2 Console's documentation, which in turn will create an AntPathRequestMatcher for you.

@Bean
public SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) {
    // ...
    http.authorizeHttpRequests((authorize) -> authorize
        .requestMatchers(PathRequest.toH2Console()).permitAll()
        // ...
    );
}

Comment From: sfoxall

@marcusdacoregio Thanks for looking into this and solving really appreciate it!

Comment From: marcusdacoregio

I'm glad I could help, I'll close this as solved. Have a good week!