I am having an issue with the filter order when using a custom auto-configured web security configuration from a different project together with Spring Cloud Eureka Server. When using the custom auto-configured web security config, the springSecurityFilterChain appears after the servletContainer filter that is created by the EurekaServerConfiguration. Hence, access to the Eureka dashboard is secured but /eureka/* do not have the Spring Security filter chain applied.
If I use the default Spring Boot auto-configured Web Security (i.e security.basic.* properties, the filter order is correct.
If I use a create a custom web security configuration in the same project and a different configuration class as the Discovery Server, the filter order is correct. If I annotate the main application class with #EnableWebSecurity or add a static inner class within the main application class with #EnableWebSecurity I also get the incorrect filter order.
What am I incorrectly providing the auto-configuration or missing something in the implementation details of the web security configuration?
A repository with full examples: Eureka Security Config Examples
Setup
module - my-autoconfiguration
#Configuration
#AutoConfigureBefore(SecurityAutoConfiguration.class)
#ConditionalOnClass(EnableWebSecurity.class)
public class CustomSecurityConfiguration {
#Configuration
#EnableWebSecurity
public static class WebSecurity extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated().and().httpBasic();
}
}
}
This project contains a META-INF folder with a spring.factories file
and the appropriate class added under the EnableAutoConfiguration key. I have verified via -Ddebug that the class is a positive match.
module - discovery-server
I include the above module as a dependency as well as spring-boot-starter-security.
#SpringBootApplication
#EnableEurekaServer
public class DiscoveryServerApplication {
public static void main(String[] args) {
SpringApplication.run(DiscoveryServerApplication.class, args);
}
}
Filter Order - Expected (when using Spring Boot basic security or custom security within discovery-server project)
Mapping filter: 'metricFilter' to: [/*]
Mapping filter: 'characterEncodingFilter' to: [/*]
Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
Mapping filter: 'springSecurityFilterChain' to: [/*]
Mapping filter: 'webRequestTraceFilter' to: [/*]
Mapping filter: 'servletContainer' to urls: [/eureka/*]
Mapping filter: 'applicationContextIdFilter' to: [/*]
Mapping servlet: 'dispatcherServlet' to [/]
Filter Order - Result (custom auto-configured web security)
Mapping filter: 'metricFilter' to: [/*]
Mapping filter: 'characterEncodingFilter' to: [/*]
Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
Mapping filter: 'webRequestTraceFilter' to: [/*]
Mapping filter: 'servletContainer' to urls: [/eureka/*]
Mapping filter: 'springSecurityFilterChain' to: [/*]
Mapping filter: 'applicationContextIdFilter' to: [/*]
Mapping servlet: 'dispatcherServlet' to [/]
The problem here is when you use the #EnableWebSecurity annotation, it disables the automatic configuration of security provided by Spring Boot in SpringBootWebSecurityConfiguration (see http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-security).
When using #EnableWebSecurity, the springSecurityFilterChain bean is instantiated in WebSecurityConfiguration. Since this bean is a plain Filter, it does not have an Order. When using the automatic configuration provided by SpringBootWebSecurityConfiguration, the springSecurityFilterChain bean is wrapped in a FilterRegistrationBean with an order that you can specify in your configuration files or a default of 0 (see https://github.com/spring-projects/spring-boot/issues/1640 for more details).
When using Spring Boot, it tries to fetch the order of the Filter before wiring them into the container. When using #EnableWebSecurity, since springSecurityFilterChain bean has no order, it will get a default value of Ordered.LOWEST_PRECEDENCE in the class OrderComparator. This is the same value of the jersey bean defined in EurekaServerConfiguration who is responsible for the calls made to the /eureka prefix. In the bootstrap phase of Eureka, the jersey bean is instantiated before the unordered springSecurityFilterChain and will get wired before in the filter chain, hence disabling the security on the /eureka prefix.
To fix the problem, simply add a FilterRegistrationBean wrapping the springSecurityChain bean in your Configuration class annotated with #EnableWebSecurity :
#Bean
#ConditionalOnBean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public FilterRegistrationBean securityFilterChainRegistration(#Qualifier(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) Filter securityFilter)
{
FilterRegistrationBean registration = new FilterRegistrationBean(securityFilter);
registration.setOrder(SecurityProperties.DEFAULT_FILTER_ORDER);
registration.setName(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME);
return registration;
}
I've put the default order of 0 here, you could make it configurable like they do in the SpringBootWebSecurityConfiguration. This will make the springSecurityChain bean as if it was autoconfigured.
Related
I'm using Spring Security for Single Sign On implementation. I've added the Spring Security filter chain in my application with annotations, and not in the web.xml.
I want the Spring Security filter chain to only apply to the /saml URLs, and I don't want the /myapi requests to go through Spring Security. But my /myapi requests are still going through the Spring Security filter chain, and Spring's filters.
Here's my code for Spring Security filter registration:
#Configuration
#EnableWebSecurity
public class SecurityConfiguration {
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.addFilterBefore(new SamlExtensionUrlForwardingFilter(), DisableEncodeUrlFilter.class)
.authorizeHttpRequests((authorize) -> authorize
.antMatchers("/saml/*").authenticated()
.antMatchers("/myapi/*").permitAll()
)
.saml2Login()
.successHandler(new MyAuthenticationSuccessHandler());
return http.build();
}
#Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().antMatchers("/myapi/*");
}
With the webSecurityCustomizer(), I tried to exclude the /myapi/ endpoints from the Spring Security workflow, and bypass the Spring Security filter chain and other Spring Security filters for the endpoint.
Additionally, I tried to get the same results with the following as well, assuming that permitAll() would mean Spring Security authorization is not required for the /myapi requests:
.antMatchers("/myapi/*").permitAll()
Neither of the above two worked, and the /myapi requests still go through Spring'a filters like DisableEncodeUrlFilter, CsrfFilter etc. Ideally, I want only /saml/* requests to go through the Spring Security filter chain and Spring related filters.
I have configured my SecurityFilterChain thus:
#EnableWebSecurity
public class WebSecurityConfig {
....
#Bean
public SecurityFilterChain configure(final HttpSecurity http) throws Exception {
http
.csrf().disable()
.cors().disable()
.authorizeRequests()
.antMatchers(HttpMethod.DELETE, "/api/user/*").access("hasRole('ADMIN')")
.antMatchers(HttpMethod.POST, "/api/user").access("hasRole('ADMIN')")
.antMatchers("/auth/login").anonymous()
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
Yet, the URL paths are open to any authenticated user irregardless of the assigned role.
I have debugged the request filter to confirm that the Principal has the right role yet a USER role can call the protected URLs successfully.
I am using Spring Boot 2.7.5.
If the path you're calling matches the authorization rule that you've declared as the last one (i.e. anyRequest().authenticated()), that implies that your test-requests don't match any of your rules that are meant to guard URLs that should be accessible only for Admins, namely:
.antMatchers(HttpMethod.DELETE, "/api/user/*").access("hasRole('ADMIN')")
.antMatchers(HttpMethod.POST, "/api/user").access("hasRole('ADMIN')")
Reminder: the matching rule declared first always weens
So, either HTTP-method or URL don't match (or both). For instance, if you're sending GET request, these restrictions would not be applied.
Regarding the URL, it should match exactly because you're using antMatchers(). I.e. path "/api/user" would not match other existing aliases of that path like "/api/user/" (more on that see here).
That's one of the reasons why in Spring Security 6.0 antMatchers() (as well as mvcMathcers() and regexMatchers()) have been removed from the API and replaced requestMatchers().
So make sure that HTTP-method is correct and path you're calling matchers exactly, and consider updating the Spring dependencies and switching to using new request-securing methods.
If you have no planes to update soon, then you can make use of the mvcMatchers(), which use Spring MVC matching rules (i.e. they take into consideration all the existing aliases of the given path), instead of antMatchers().
Here's an example of how your configuration might be implemented with Spring Security 6.0 and Lambda DSL (if you feel more comfortable with chaining configuration options using and() this flavor of DSL is still supported as well):
#Configuration
public class SecurityConfig {
#Bean
public SecurityFilterChain configure(final HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.DELETE, "/api/user/*").hasRole("ADMIN") // in Spring Security 6.0 method access() has been changed, and you don't need it anyway to verify the Role
.requestMatchers(HttpMethod.POST, "/api/user").hasRole("ADMIN")
.requestMatchers("/auth/login").anonymous()
.anyRequest().authenticated()
)
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}
I want to using #PreAuthorize with SpEL within Spring Security like the example at http://forum.spring.io/forum/spring-projects/security/100708-spel-and-spring-security-3-accessing-bean-reference-in-preauthorize
but this is not work for me while using it in Spring Security 4.1.4. Below is my sample code:
A bean class:
package com.service.spel;
import org.springframework.stereotype.Component;
#Component(value="accessBean")
public class AccessCheckBean {
public String getPermision(){
/**
* return the api permision
*/
String scope = "#oauth2.hasScope('resource.Update')";
return scope;
}
}
In controller:
#PreAuthorize("accessBean.getPermision()")
#GetMapping("/perm")
public #ResponseBody String getPerm(){
return "Perm";
}
Error message:
Failed to evaluate expression 'accessBean.getPermision()'
seems I can't use SpEL like above, then if I am using this version of Spring Security, how can I proceed?
You have to use #, see Spring Security Reference:
Referring to Beans in Web Security Expressions
If you wish to extend the expressions that are available, you can easily refer to any Spring Bean you expose. For example, assuming you have a Bean with the name of webSecurity that contains the following method signature:
public class WebSecurity {
public boolean check(Authentication authentication, HttpServletRequest request) {
...
}
}
You could refer to the method using:
<http>
<intercept-url pattern="/user/**"
access="#webSecurity.check(authentication,request)"/>
...
</http>
or in Java configuration
http
.authorizeRequests()
.antMatchers("/user/**").access("#webSecurity.check(authentication,request)")
...
While testing Spring Boot (1.3.3) with a simple web app using spring-boot-starter-security:1.3.3:RELEASE I observed the following behaviour:
In order to override the default Spring web security configuration, I supplied a custom Java configuration class like so:
#Configuration
// #EnableWebSecurity apparently obsolete ?
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
// http security checking left out for brevity ...
}
#Override
protected void configure(
AuthenticationManagerBuilder auth) throws Exception {
// user authentication left out for brevity ...
}
}
After startup, the application redirects to the login page and checks username/password correctly whether the #EnableWebSecurity annotation is provided or not (like in the example above). Is this annotation in this context therefore obsolete ? If so, why ?
The auto configuration of Spring Boot automatically enables web security and retrieves all beans of the type WebSecurityConfigurerAdapter to customize the configuration if certain conditions are met (spring-boot-starter-security on the classpath etc.). The auto configuration for web security is enabled in the class org.springframework.boot.autoconfigure.security.SpringBootWebSecurityConfiguration (Spring Boot 1.2.7, class name may have changed in newer versions).
I'm having some trouble testing a Spring Boot application with MockMvc.
I have the following test class:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = {SpringConfiguration.class, SecurityConfiguration.class})
#IntegrationTest({"server.port=8080"})
#WebAppConfiguration
public class DemoTest {
#Autowired
private EmbeddedWebApplicationContext webApplicationContext;
private MockMvc mockMvc;
#Before
public void setUp() throws Exception {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
#Test
public void testGetAccountUnauthenticated() throws Exception {
mockMvc.perform(get("/accounts/1").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isUnauthorized());
}
}
This results in a HTTP 200 not a 401. I have component scanning and autoconfiguration enabled and spring security is configured in my SecuityConfiguration class as follows:
#Configuration
#EnableWebSecurity
#EnableWebMvcSecurity // required for use of #AuthenticationPrincipal in MVC controllers.
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
public void configure(WebSecurity web) {
web.debug(true);
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
//set up authentication.
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
// set up form login
}
}
If I use a RestTemplate to access http://localhost:8080/accounts/1 then I get the expected behaviour (HTTP 401).
I have seen other examples (e.g. Spring Boot setup security for testing) that suggest that I autowire the FilterChainProxy and add the filter manually using the WebApplicationContext.addFilters(filterChainProxy) method. However, this actually fails for me (org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [org.springframework.security.web.FilterChainProxy] found).
I have two questions:
Why does the injected WebApplicationContext not automatically use the SpringSecurity filters? Even if I could get the FilterChainProxy and add it manually, the JavaDoc for EmbeddedWebApplicationContext states
any {#link Servlet} or {#link Filter} beans defined in the context will be automatically registered with the embedded Servlet container
As a result I wouldn't expect to have to manually add the security filter chain since I (incorrectly?) expect this to "just work" due to the Auto Configuration magic in Spring Boot?
Why is there no FilterChainProxy in the application context? Again, perhaps my expectations of the AutoConfiguration is incorrect - but I thought that this would be configured as part of the context configuration.
Thanks in advance for any advice.
Edits
The reason a FilterChainProxy doesn't get injected was because I has my configuration set to
public void configure(WebSecurity web) {
web.debug(true);
}
This actually configures a org.springframework.security.web.debug.DebugFilter instead. The way I have now managed to get the Filter regardless of this debug setting is as follows:
#Resource(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
private Filter securityFilter;
If I add this to the MockMvcBuilder as follows:
MockMvcBuilders.webAppContextSetup(webApplicationContext).addFilters(securityFilter)
then it does work as expected.
But, I don't understand why MockMVC would ignore the filters as this seems important for testing a request since anything could happen in a Filter that might impact the outcome of the test. Furthermore, it means that to test properly I'd need to lookup all Filters in the servlet context and establish their priority/url mapping and add them appropriately. This seems error prone and unnecessary.
I agree that MockMVC is perhaps more for testing SpringMVC and custom code in controllers, as commented by #dave-syer. So in cases when one wants to test spring MVC infrastructure with your custom controller code at the same time (correctness of controllers mapped to URLs; mapping and validation of input and output objects; standard controllers; your controllers) without leveraging the Servlet container part of the stack, MockMVC is there for you.
But MockMVC also does have methods to add filters, so it is designed with a possibility to engage Filters in the described type of testing. Sometimes filter may play functional role for code inside of a controller and that would be otherwise not testable with MockMVC.
With all that theory in mind I was trying to mimic Boot behaviour for my tests where filters would be set up in Spring Boot way and picked up by my tests to be used with MockVMC. Here is a snippet that I ended up using. It can surely be enhanced to mimic Boot behaviour in more precisely and extracted to some custom MockMVCBuilder.
#Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
#Before
public void setUp() {
Collection<Filter> filterCollection = wac.getBeansOfType(Filter.class).values();
Filter[] filters = filterCollection.toArray(new Filter[filterCollection.size()]);
mockMvc = MockMvcBuilders.webAppContextSetup(wac).addFilters(filters).build();
}
Have you tried this?
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
...
#Slf4j
#RunWith(SpringRunner.class)
#SpringBootTest
public class AuthorizeTest {
#Autowired
private WebApplicationContext wac;
#Before
public void setup() {
this.mockMvc = MockMvcBuilders
.webAppContextSetup(wac)
.apply(springSecurity())
.build();
}
...
}
In my case it is 403, not 401, but you get the idea.