I use Spring Security and an Oauth 2 Resource Server within a Spring Cloud Gateway project. I have configured to allow some endpoints without authentication. Eg.: /actuator/health
Authentication check is handled correctly and /actuator API calls are going trough without Authentication Header but still the exceptionHandling ServerAuthenticationEntryPoint is called which logs an error in my case.
I see that ServerAuthenticationEntryPoint.commence is called even twice on not permitted APIs and on permitted APIs it's called a single time. So something is calling the ServerAuthenticationEntryPoint.commence a second time.
Sample Config:
I tried via permitAll:
@EnableWebFluxSecurity
@EnableMethodSecurity
@Configuration
public class SecurityConfig {
@Bean
SecurityWebFilterChain springSecurityFilterChain(final ServerHttpSecurity http) {
return http
//Disable Http Basic Auth and CSRF (not required because no cookies are used)
.httpBasic().disable().csrf().disable()
//All request need to be authenticated except OPTIONS and /actuator/**
.authorizeExchange()
//Allow all actuator calls
.pathMatchers("/actuator/**").permitAll()
//Any other exchange need to be authenticated
.anyExchange().authenticated()
//Do not allow sessions
.and().securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
//Exception Handling
.exceptionHandling().authenticationEntryPoint(new AuthenticationErrorHandler())
//JWT validation via OAuth Resource Server
.and().oauth2ResourceServer().jwt().build();
}
}
And via NegatedServerWebExchangeMatcher:
@EnableWebFluxSecurity
@EnableMethodSecurity
@Configuration
public class SecurityConfig {
@Bean
SecurityWebFilterChain springSecurityFilterChain(final ServerHttpSecurity http) {
return http
//Disable Http Basic Auth and CSRF (not required because no cookies are used)
.httpBasic().disable().csrf().disable()
//Allow all actuator calls
.securityMatcher(new NegatedServerWebExchangeMatcher(
ServerWebExchangeMatchers.pathMatchers("/actuator/**")))
.authorizeExchange()
//Any other exchange need to be authenticated
.anyExchange().authenticated()
//Do not allow sessions
.and().securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
//Exception Handling
.exceptionHandling().authenticationEntryPoint(new AuthenticationErrorHandler()).and()
//JWT validation via OAuth Resource Server
.oauth2ResourceServer().jwt().build();
}
}
Comment From: marcusdacoregio
Hi @Hollerweger, thanks for the report.
If there is an AuthenticationException on a "permitted" endpoint, the entry point would be invoked. Have you tried creating two filter chains, one for the actuator and one for the rest?
@Bean
@Order(0)
SecurityWebFilterChain actuatorFilterChain(final ServerHttpSecurity http) {
http
.securityMatcher(ServerWebExchangeMatchers.pathMatchers("/actuator/**"))
.authorizeExchange(authorize -> authorize
.pathMatchers("/actuator/health").permitAll()
.anyRequest().denyAll()
);
return http.build();
}
@Bean
@Order(1)
SecurityWebFilterChain springSecurityFilterChain(final ServerHttpSecurity http) {
// your original configuration
return http.build();
}
Comment From: Hollerweger
HI @marcusdacoregio, thanks for the quick response.
Can you explain why do I need the securityMatcher before the authorizeExchange and path matchers in first security filter chain.
For webflux the interface looks to be a bit different and expects a ServerWebExchangeMatcher:
public ServerHttpSecurity securityMatcher(ServerWebExchangeMatcher matcher)
Comment From: marcusdacoregio
Can you explain why do I need the securityMatcher before the authorizeExchange and path matchers in first security filter chain.
The securityMatcher tells Spring Security that the filter chain should only be invoked for requests that start with a path equal to /actuator. It allows you to provide different configuration/authentication mechanisms based on the path. The actuatorFilterChain has no authentication mechanism since it permits all on /actuator/health and every other request is denied by default.
For webflux the interface looks to be a bit different and expects a ServerWebExchangeMatcher:
You can use ServerWebExchangeMatchers.pathMatchers("/actuator/**")
Comment From: Hollerweger
Thanks, I have adapted the security filter but i still see that the .exceptionHandling().authenticationEntryPoint is getting called.
@Bean
@Order(0)
SecurityWebFilterChain permittedChain(final ServerHttpSecurity http) {
//TODO: Need to allow also all OPTIONS calls here
http.securityMatcher(ServerWebExchangeMatchers.pathMatchers("/actuator/**"))
.authorizeExchange()
.pathMatchers("/actuator/**")
.permitAll()
.anyExchange()
.denyAll();
return http.build();
}
@Bean
@Order(1)
SecurityWebFilterChain springSecurityFilterChain(final ServerHttpSecurity http) {
http
//Disable Http Basic Auth and CSRF (not required because no cookies are used)
.httpBasic().disable().csrf().disable()
//All request need to be authenticated except OPTIONS and /actuator/**
.authorizeExchange()
//Any other exchange need to be authenticated
.anyExchange().authenticated()
//Do not allow sessions
.and().securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
//Exception Handling
.exceptionHandling().authenticationEntryPoint(new AuthenticationErrorHandler())
//JWT validation via OAuth Resource Server
.and().oauth2ResourceServer().jwt();
return http.build();
}
Comment From: Hollerweger
Ah okay I think I found the issue in above code:
I used url routing in cloud gateway and also need to include the routing predicate in the securityMatcher. Eg.: /*/actuator/**
@Bean
@Order(0)
SecurityWebFilterChain permittedChain(final ServerHttpSecurity http) {
//TODO: Need to allow also all OPTIONS calls here
http.securityMatcher(ServerWebExchangeMatchers.pathMatchers("/actuator/**", "/*/actuator/**"))
.authorizeExchange()
.pathMatchers("/actuator/**")
.permitAll()
.pathMatchers("/*/actuator/**")
.permitAll()
.anyExchange()
.denyAll();
return http.build();
}
Comment From: Hollerweger
@marcusdacoregio Working now, thanks!
Do I need to add another Chain for allowing all OPTIONS calls? Not sure how I can apply the securityMatcher only for OPTIONS calls...
Comment From: marcusdacoregio
Usually, there is no need to allow OPTIONS calls. The CorsFilter stops the filter chain if it is a pre-flight request.