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!