Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Page with permitAll is no longer accessible via auto-configured MockMvc #28759

Closed
martinvisser opened this issue Nov 20, 2021 · 8 comments
Closed
Labels
type: regression A regression from a previous release
Milestone

Comments

@martinvisser
Copy link

As asked on gitter I found an issue after upgrading to Spring Boot 2.6.0: running a @SpringBootTest with @AutoConfigureMockMvc a login page (not limited to) is no longer accessible after the upgrade. The same configuration that worked on Spring Boot 2.5.7 now triggers a 401. Tracing this lead me to the ErrorPageSecurityFilter.

Since 2.6.0 the initial request gets granted, but the (mock) filter chain goes through the ErrorPageSecurityFilter and denies in later.

I made a small example project to reproduce the issue: https://github.com/martinvisser/error-page-security-filter-issue.

For reasons I can't exactly remember I had multiple configuration extending from WebSecurityConfigurerAdapter which worked in Spring Boot 2.5.7. Merging the two configurations into one fixed the issue for me, but it does still sound like unforeseen and unwanted behavior.

This worked in 2.5.7, but fails in 2.6.0:

@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
internal class WebSecurityConfig : WebSecurityConfigurerAdapter() {
    @Configuration(proxyBeanMethods = false)
    @Order(1)
    internal class FormWebSecurityConfigurerAdapter : WebSecurityConfigurerAdapter() {
        override fun configure(http: HttpSecurity) {
            http.authorizeRequests {
                it.anyRequest().permitAll()
            }
        }
    }
}

This works in both though:

@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
internal class WebSecurityConfig : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        http.authorizeRequests {
            it.anyRequest().permitAll()
        }
    }
}
@martinvisser
Copy link
Author

Perhaps related to #26356 and/or #28741

@wilkinsona
Copy link
Member

Thanks for the sample, @martinvisser.

The cause of the problem is that ErrorPageSecurityFilter is being called for a REQUEST dispatch despite only being registered for ERROR dispatches. This is a limitation of MockMvc that I had overlooked when working on the changes for #26356 with @mbhave. I've opened spring-projects/spring-framework#27717 so that the Framework team can consider an enhancement to MockMvc that would allow us to register filters for particular dispatcher types. In the meantime, we can update Boot's ErrorPageSecurityFilter to ignore non-ERROR requests.

@wilkinsona wilkinsona added type: regression A regression from a previous release and removed status: waiting-for-triage An issue we've not yet triaged labels Nov 22, 2021
@wilkinsona wilkinsona changed the title Page with permitAll no longer accessible via mockMvc after upgrade to 2.6.0 Page with permitAll no longer accessible via MockMvc after upgrade to 2.6.0 Nov 22, 2021
@wilkinsona wilkinsona added this to the 2.6.1 milestone Nov 22, 2021
@wilkinsona wilkinsona changed the title Page with permitAll no longer accessible via MockMvc after upgrade to 2.6.0 Page with permitAll no longer accessible via auto-configured MockMvc after upgrade to 2.6.0 Nov 22, 2021
@wilkinsona
Copy link
Member

A workaround for this problem is to remove the error page security filter by adding the following bean to the configuration used in your application's tests:

@Bean
static BeanFactoryPostProcessor removeErrorSecurityFilter() {
    return (beanFactory) -> 
        ((DefaultListableBeanFactory)beanFactory).removeBeanDefinition("errorPageSecurityInterceptor");
}

@steklopod
Copy link

I also have the same problem with 2.6.0 only:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
class SecurityConfig(private val userService: UserService) : WebSecurityConfigurerAdapter() {
    companion object {
        private val whitelist = arrayOf(
            "/registration", "/registration/*", "/login", "/user/exists/*"
        )
    }


    override fun configure(webSecurity: WebSecurity)           { webSecurity.ignoring().antMatchers(*whitelist) }
    override fun configure(auth: AuthenticationManagerBuilder) { auth.authenticationProvider(authProvider()) }

    @Bean override fun authenticationManagerBean(): AuthenticationManager = super.authenticationManagerBean()
    @Bean fun passwordEncoder(): PasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder()
    @Bean fun authProvider   (): DaoAuthenticationProvider = DaoAuthenticationProvider().apply { setUserDetailsService(userService); setPasswordEncoder(BCryptPasswordEncoder()) }
   

Result

java.lang.AssertionError: Status expected:<200> but was:<401>
Expected :200
Actual   :401

My test:

@SpringBootTest
@AutoConfigureMockMvc
internal class LoginControllerTest(
    @Autowired private val mockMvc: MockMvc,
    @Autowired private val userService: UserService,
) {
    private val loginUrl = "/login"
    private val email = """email@gmail.com"""
    private val principal = """{ "email": "$email", "password": "test" }"""


    @Test
    fun `login and logout`() {
        val result = mockMvc.perform(
            post(loginUrl).contentType(APPLICATION_JSON)
                .content(principal)
        )
            .andDo(MockMvcResultHandlers.print())
            .andExpect(status().isOk)
}

@wilkinsona
Copy link
Member

@steklopod That does look like the same problem. If you haven't done so already, please try the workaround.

@steklopod
Copy link

Thank you. It solved my problem. I just put it my @Configuration file in backend/src/test/kotlin/config/AnyTestConfig.kt:

Kotlin bean:

    @Bean
    fun removeErrorSecurityFilter(): BeanFactoryPostProcessor =
        BeanFactoryPostProcessor { beanFactory: ConfigurableListableBeanFactory ->
            (beanFactory as DefaultListableBeanFactory).removeBeanDefinition("errorPageSecurityInterceptor")
        }

@fast-reflexes
Copy link

fast-reflexes commented Nov 24, 2021

I opened a similar ticket in Spring Security's Github repo: spring-projects/spring-security#10544.
I also use multiple configurations and test repo is available here: https://github.com/fast-reflexes/spring-boot-bug

So...

  • Root to problem found ✅
  • Long-term and short-term fixes underway ✅
  • Reason to problem ❌

I like to understand what's going on in Spring and especially security-wise, but here I feel a little lost:

1. Finding the root of the problem

I have turned on debugging on all my configs and set logging.level.org=TRACE and logging.level.com=TRACE and log all output streams while running a single test which used to pass and now fails. The only place where I see EITHER ErrorPageSecurityFilter or errorPageSecurityInterceptor (the filter registration bean) connected to this failed test is in the stacktrace:

org.springframework.security.access.AccessDeniedException: Access is denied
at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:73) ~[spring-security-core-5.6.0.jar:5.6.0]
at org.springframework.security.web.access.DefaultWebInvocationPrivilegeEvaluator.isAllowed(DefaultWebInvocationPrivilegeEvaluator.java:100) ~[spring-security-web-5.6.0.jar:5.6.0]
at org.springframework.security.web.access.DefaultWebInvocationPrivilegeEvaluator.isAllowed(DefaultWebInvocationPrivilegeEvaluator.java:67) ~[spring-security-web-5.6.0.jar:5.6.0]
at org.springframework.boot.web.servlet.filter.ErrorPageSecurityFilter.doFilter(ErrorPageSecurityFilter.java:58) ~[spring-boot-2.6.0.jar:2.6.0]
at javax.servlet.http.HttpFilter.doFilter(HttpFilter.java:57) ~[tomcat-embed-core-9.0.55.jar:4.0.FR]
    ....

Apart from that, the errorPageSecurityInterceptor is mentioned ONCE in quite a load of output :)

Was is from this that you found that this filter was the culprit or did I miss something?

2. What does ErrorPageSecurityFilter solve?

I've read the tickets about this problem but I still don't get it. So there used to be some discrepancy between how error messages were sent in connection to endpoints requiring authentication; if no credentials were given, you got a more juicy error message than when you supplied incorrect credentials. Seems a bit odd, I agree with that, but why did it take a new filter to solve it and exactly how does this filter solve it?

3. Role in the filter chain

As you understand, I did quite some debugging, but feel puzzled about the behaviour, even given this ErrorPageSecurityFilter. I've understood that this filter is installed outside of Spring Security and that it runs among the filters. I guess then that it is not Spring Security who denies access?

This is my error message with multiple configs where the lowest-order config denies all access (catch-all):

2021-11-23 10:24:42.450 TRACE 82423 --- [    Test worker] o.s.security.web.FilterChainProxy        : Invoking FilterSecurityInterceptor (12/12)
2021-11-23 10:24:42.450 TRACE 82423 --- [    Test worker] o.s.s.w.a.i.FilterSecurityInterceptor    : Did not re-authenticate UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=user, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_API_USER]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[ROLE_API_USER]] before authorizing
2021-11-23 10:24:42.450 TRACE 82423 --- [    Test worker] o.s.s.w.a.i.FilterSecurityInterceptor    : Authorizing filter invocation [GET /api/bogus] with attributes [hasAnyRole('ROLE_API_USER')]
2021-11-23 10:24:42.450 DEBUG 82423 --- [    Test worker] o.s.s.w.a.i.FilterSecurityInterceptor    : Authorized filter invocation [GET /api/bogus] with attributes [hasAnyRole('ROLE_API_USER')]
2021-11-23 10:24:42.450 TRACE 82423 --- [    Test worker] o.s.s.w.a.i.FilterSecurityInterceptor    : Did not switch RunAs authentication since RunAsManager returned null
2021-11-23 10:24:42.450 DEBUG 82423 --- [    Test worker] o.s.security.web.FilterChainProxy        : Secured GET /api/bogus
2021-11-23 10:24:42.450 TRACE 82423 --- [    Test worker] o.s.s.w.a.expression.WebExpressionVoter  : Voted to deny authorization
2021-11-23 10:24:42.455 DEBUG 82423 --- [    Test worker] a.DefaultWebInvocationPrivilegeEvaluator : filter invocation [/api/bogus] denied for UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=user, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_API_USER]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[ROLE_API_USER]]

    org.springframework.security.access.AccessDeniedException: Access is denied
        at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:73) ~[spring-security-core-5.6.0.jar:5.6.0]
        ...

2021-11-23 10:24:42.456 TRACE 82423 --- [    Test worker] o.s.s.w.header.writers.HstsHeaderWriter  : Not injecting HSTS header since it did not match request to [Is Secure]

Without the last deny-all config, the above is turned into:

2021-11-23 10:28:24.524 TRACE 82954 --- [    Test worker] o.s.security.web.FilterChainProxy        : Invoking FilterSecurityInterceptor (12/12)
2021-11-23 10:28:24.524 TRACE 82954 --- [    Test worker] o.s.s.w.a.i.FilterSecurityInterceptor    : Did not re-authenticate UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=user, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_API_USER]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[ROLE_API_USER]] before authorizing
2021-11-23 10:28:24.524 TRACE 82954 --- [    Test worker] o.s.s.w.a.i.FilterSecurityInterceptor    : Authorizing filter invocation [GET /api/bogus] with attributes [hasAnyRole('ROLE_API_USER')]
2021-11-23 10:28:24.524 DEBUG 82954 --- [    Test worker] o.s.s.w.a.i.FilterSecurityInterceptor    : Authorized filter invocation [GET /api/bogus] with attributes [hasAnyRole('ROLE_API_USER')]
2021-11-23 10:28:24.525 TRACE 82954 --- [    Test worker] o.s.s.w.a.i.FilterSecurityInterceptor    : Did not switch RunAs authentication since RunAsManager returned null
2021-11-23 10:28:24.525 DEBUG 82954 --- [    Test worker] o.s.security.web.FilterChainProxy        : Secured GET /api/bogus
2021-11-23 10:28:24.525 TRACE 82954 --- [    Test worker] o.s.t.web.servlet.TestDispatcherServlet  : GET "/api/bogus", parameters={}, headers={masked} in DispatcherServlet ''
2021-11-23 10:28:24.526 TRACE 82954 --- [    Test worker] o.s.b.f.s.DefaultListableBeanFactory     : Returning cached instance of singleton bean 'exampleEndpoint'
2021-11-23 10:28:24.526 TRACE 82954 --- [    Test worker] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.examplespringboot.ExampleEndpoint#bogus()
2021-11-23 10:28:24.526 TRACE 82954 --- [    Test worker] o.s.web.method.HandlerMethod             : Arguments: []
2021-11-23 10:28:24.527 DEBUG 82954 --- [    Test worker] m.m.a.RequestResponseBodyMethodProcessor : Using 'text/plain', given [*/*] and supported [text/plain, */*, text/plain, */*, application/json, application/*+json, application/json, application/*+json]
2021-11-23 10:28:24.527 TRACE 82954 --- [    Test worker] m.m.a.RequestResponseBodyMethodProcessor : Writing ["bogus"]
2021-11-23 10:28:24.528 TRACE 82954 --- [    Test worker] o.s.s.w.header.writers.HstsHeaderWriter  : Not injecting HSTS header since it did not match request to [Is Secure]

So it seems to me that in both cases Spring Security says YES, but in the first case, there is some problem when the ErrorPageSecurityFilter processes the request, but exactly what is the problem and why does it disappear once I remove the deny-all config? It seems ErrorPageSecurityFilter invokes DefaultWebInvocationPrivilegeEvaluator. This in turn seems to somehow call WebExpressionVoter (via some other class) which turns our request down... Why? And what is this class used for otherwise?

Note that I DO have an entry for /error path in my security config and it allows access to all.

I would LOVE to understand this better so if you have time, feel free to fill me in :)

@philwebb philwebb changed the title Page with permitAll no longer accessible via auto-configured MockMvc after upgrade to 2.6.0 Page with permitAll is no longer accessible via auto-configured MockMvc Nov 24, 2021
jzheaux added a commit to spring-attic/spring-security-oauth2-boot that referenced this issue Nov 24, 2021
@fast-reflexes
Copy link

fast-reflexes commented Nov 26, 2021

I investigated the matter myself and found an additional bug. Explanations to the above and info about this bug can be found at #28818

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: regression A regression from a previous release
Projects
None yet
Development

No branches or pull requests

5 participants