spring-projects / spring-security

Spring Security
http://spring.io/projects/spring-security
Apache License 2.0
8.5k stars 5.78k forks source link

SecurityContextHolderFilter does not apply to async dispatch #11962

Closed jkjome closed 1 year ago

jkjome commented 1 year ago

Describe the bug

As mentioned in gitter...

My app currently runs on Spring Boot 2.7.4. I was testing compatibility with 3.0.0-M5. All appeared to work well except one aspect of spring-security, as part of presenting a client certificate using WebClient. I end up with "AccessDeniedException: Access is denied" thrown by AffirmativeBased.decide(AffirmativeBased.java:73). The handshake appears to work fine, and the handshake logging looks nearly identical to that under 2.7.4. But I got it to work after downgrading to spring-security:6.0.0-M5 (from M7). So, it seems something broke as of spring-security:6.0.0-M6. Is this a known issue, and will it be fixed in the next spring-security release?

To Reproduce

  1. Under standard configuration of Spring Boot 3.0.0-M5, which includes spring-security:6.0.0-M7 (or even downgrading to M6), run the code below against a URL resource protected by client certificate authorization
  2. Witness AccessDeniedException: Access is denied" thrown by AffirmativeBased.decide(AffirmativeBased.java:73)

Expected behavior

Client certificate authentication succeeds... as it does under both Spring Boot 2.7.4 and 3.0.0-M5 with spring-security downgraded to 6.0.0-M5 (from 6.0.0-M7)

Sample

private StreamingResponseBody streamUrl(final String url) {
    final String keyStoreResourcePath = "[url resource path to key/trust store]", keyStorePass = "[some password]";
    final HttpClient httpClient = HttpClient.create().secure(spec -> {
        try {
            final char[] keyStorePassChars = keyStorePass.toCharArray();
            final URL keyStoreUrl = ResourceUtils.getURL(keyStoreResourcePath);
            final KeyStore keyStore = KeyStore.getInstance("PKCS12");
            keyStore.load(keyStoreUrl.openStream(), keyStorePassChars);
            final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
            keyManagerFactory.init(keyStore, keyStorePassChars);
            final TrustManagerFactory trustManagerFactory = InsecureTrustManagerFactory.INSTANCE;
            spec.sslContext(Http2SslContextSpec.forClient().configure(sslContextBuilder -> sslContextBuilder.keyManager(keyManagerFactory).trustManager(trustManagerFactory)));
        } catch (final NoSuchAlgorithmException | KeyStoreException | CertificateException | UnrecoverableKeyException | IOException e) {
            throw new IllegalStateException("Boom!", e);
        }
    });
    final WebClient client = WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient)).baseUrl(url).build();
    final Flux<DataBuffer> dataBufferFlux = client.get()
            .accept(MediaType.APPLICATION_PDF)
            .retrieve()
            .bodyToFlux(DataBuffer.class);
    return out -> DataBufferUtils.write(dataBufferFlux, out).doOnNext(DataBufferUtils.releaseConsumer()).blockLast(Duration.ofSeconds(20));
}

@GetMapping(path = "/docs/{docId}", produces = MediaType.APPLICATION_PDF_VALUE)
public ResponseEntity<StreamingResponseBody> stream(@PathVariable final String docId) {
    final String url = lookupUrlGivenDocId(docId); //Some client cert auth protected document URL being streamed on behalf of the client
    return ResponseEntity.ok().contentType(MediaType.APPLICATION_PDF).body(streamUrl(url));
}
sjohnr commented 1 year ago

Hi @jkjome!

Is this a known issue, and will it be fixed in the next spring-security release?

Not that I'm aware of. Thanks for reaching out. There have been a number of changes recently to update defaults for various aspects of the framework.

As we're heads down preparing for this release, it would be helpful if you could provide a minimal, reproducible sample. The above code sample does not provide a way to run a reproducible example, such as a unit test or a self-contained Spring Boot application (with instructions for reproducing). Can you provide one?

jkjome commented 1 year ago

It occurred to me that my Spring Security inbound rules were affecting outbound WebClient calls. I did a search and found issue #10589 that confirmed my suspicion that something like that might be occurring. I played with some settings and found a new API that appears to resolve my issue under spring-security:6.0.0-M6+ (and wasn't necessary prior to that). It seems to me that this just shouldn't happen... and doesn't under prior versions of Spring Security.

If I provide .shouldFilterAllDispatcherTypes(false), it seems to prevent the Spring Security inbound rules from affecting the WebClient outbound calls. Making that call prior to .anyRequest().authenticated() seems to do the trick. I found this late yesterday, so I haven't had time to create a minimal reproducible example. But, I figured I'd mention the solution now, and work on the reproducible example later, to get it on your radar.

Of course, I don't know if this is the appropriate approach, nor whether something else in my config should just be changed so that .shouldFilterAllDispatcherTypes(false) becomes unnecessary? I'd appreciate if you could critique my security config and let me know if it should be changed in some fundamental way.

 @Bean
 SecurityFilterChain filterChain(final HttpSecurity http) throws Exception {
     http.authorizeHttpRequests(cstmzr -> cstmzr
             .shouldFilterAllDispatcherTypes(false) //<-- Fixes WebClient issue, but what does it do???
             .anyRequest().authenticated()
         )
         .csrf(cstmzr -> cstmzr.disable())
         .httpBasic(withDefaults());
     return http.build();
}
sjohnr commented 1 year ago

@marcusdacoregio do we have any documentation on the new method, or thoughts about the above issue?

marcusdacoregio commented 1 year ago

Yes, you can refer to this link for the docs (Example 6). I cannot see exactly what is going on right now since I'm not that familiar with WebClient but if @jkjome is able to provide a minimal, reproducible sample I can take a look at it early next week.

sjohnr commented 1 year ago

Thanks @marcusdacoregio. @jkjome it seems you’re experiencing symptoms of a new default in 6.0, but I’d like to understand the scenario as well.

I’m less familiar with dispatcher types in Spring, but I’m guessing you’re operating under another type, such as ASYNC. I think the minimal sample will help quite a bit because there’s a number of things that could be going on. I wonder for example what your client security configuration looks like, as well as whether the server you’re accessing is affecting things. I can’t tell just glancing at the code provided.

jkjome commented 1 year ago

Just wanted to mention that I can replace shouldFilterAllDispatcherTypes(false) with dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll() and it also resolves my issue.

However, it's been difficult to come up with a minimal testcase. I tried a minimal setup where I made an outbound WebClient request to an arbitrary, unprotected, HTTPS URL. It worked fine with/out having to implement either of the above workarounds. The remaining difference with my app is that the URLs I'm calling in my app are protected by Client Cert-based Auth. In fact, when I use my own certificate to call one of these protected URLs, I can reproduce the behavior. So, it appears as if the key distinction is connecting to a URL that is protected by Cient Cert-based Auth.

I've been trying to come up with my own self-signed cert to try to create a minimal testcase, but I keep running into various certificate errors attempting to use it. So far, the only way I can reproduce this is by using my own real certiificate against a URL that accepts my certificate for Client Cert-based Auth. And, obviously, I can't publish that. So, I'm hoping somone on the spring-security team will have more certificate knowledge/mojo to re-create the scenario I've described.

Below are the complete contents of the log, including stacktraces, that result when I don't use one of the above two workarounds when calling one of my Client Cert-based Auth protected URLs using WebClient. Note that doing the same with RestTemplate does not result in any errors... presumably because WebClient makes use of the ASYNC dispatcher while RestTemplate does not. The errors occur after successful completion of the TLS handshake (not included in the log below). The log ends with two SSL debug entries that, actually, occur shortly after the request whether I use one of the workarounds or not. I left them in just in case they might be of interest to someone on the spring-security team....

2022-10-12T02:10:52.280-05:00 ERROR 28772 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] threw exception

org.springframework.security.access.AccessDeniedException: Access Denied
    at org.springframework.security.web.access.intercept.AuthorizationFilter.doFilterInternal(AuthorizationFilter.java:75) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:360) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:126) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:120) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:360) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:91) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:85) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:360) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:98) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:360) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:179) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:360) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:360) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:360) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:360) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:360) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:360) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:360) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:360) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:360) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:224) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:189) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:351) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:267) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:691) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.ApplicationDispatcher.doDispatch(ApplicationDispatcher.java:612) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.ApplicationDispatcher.dispatch(ApplicationDispatcher.java:582) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.AsyncContextImpl$AsyncRunnable.run(AsyncContextImpl.java:588) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.AsyncContextImpl.doInternalDispatch(AsyncContextImpl.java:354) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:194) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:119) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.connector.CoyoteAdapter.asyncDispatch(CoyoteAdapter.java:246) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.coyote.AbstractProcessor.dispatch(AbstractProcessor.java:241) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:59) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:867) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1762) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]

2022-10-12T02:10:52.282-05:00 ERROR 28772 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Unable to handle the Spring Security Exception because the response is already committed.] with root cause

org.springframework.security.access.AccessDeniedException: Access Denied
    at org.springframework.security.web.access.intercept.AuthorizationFilter.doFilterInternal(AuthorizationFilter.java:75) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:360) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:126) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:120) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:360) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:91) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:85) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:360) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:98) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:360) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:179) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:360) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:360) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:360) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:360) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:360) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:360) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:360) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:360) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:360) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:224) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:189) ~[spring-security-web-6.0.0-M7.jar:6.0.0-M7]
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:351) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:267) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:691) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.ApplicationDispatcher.doDispatch(ApplicationDispatcher.java:612) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.ApplicationDispatcher.dispatch(ApplicationDispatcher.java:582) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.AsyncContextImpl$AsyncRunnable.run(AsyncContextImpl.java:588) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.AsyncContextImpl.doInternalDispatch(AsyncContextImpl.java:354) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:194) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:119) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.connector.CoyoteAdapter.asyncDispatch(CoyoteAdapter.java:246) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.coyote.AbstractProcessor.dispatch(AbstractProcessor.java:241) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:59) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:867) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1762) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]

2022-10-12T02:10:52.288-05:00  WARN 28772 --- [nio-8080-exec-2] o.apache.catalina.core.AsyncContextImpl  : onError() call failed for listener of type [org.apache.catalina.core.AsyncListenerWrapper]

java.lang.IllegalArgumentException: Cannot dispatch without an AsyncContext
    at org.springframework.util.Assert.notNull(Assert.java:201) ~[spring-core-6.0.0-M6.jar:6.0.0-M6]
    at org.springframework.web.context.request.async.StandardServletAsyncWebRequest.dispatch(StandardServletAsyncWebRequest.java:131) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.springframework.web.context.request.async.WebAsyncManager.setConcurrentResultAndDispatch(WebAsyncManager.java:400) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.springframework.web.context.request.async.WebAsyncManager.lambda$startCallableProcessing$2(WebAsyncManager.java:322) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.springframework.web.context.request.async.StandardServletAsyncWebRequest.lambda$onError$0(StandardServletAsyncWebRequest.java:146) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) ~[na:na]
    at org.springframework.web.context.request.async.StandardServletAsyncWebRequest.onError(StandardServletAsyncWebRequest.java:146) ~[spring-web-6.0.0-M6.jar:6.0.0-M6]
    at org.apache.catalina.core.AsyncListenerWrapper.fireOnError(AsyncListenerWrapper.java:49) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.core.AsyncContextImpl.setErrorState(AsyncContextImpl.java:413) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.catalina.connector.CoyoteAdapter.asyncDispatch(CoyoteAdapter.java:250) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.coyote.AbstractProcessor.dispatch(AbstractProcessor.java:241) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:59) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:867) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1762) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-10.0.23.jar:10.0.23]
    at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]

javax.net.ssl|DEBUG|53|reactor-http-nio-2|2022-10-12 02:11:22.252 CDT|Alert.java:238|Received alert message (
"Alert": {
  "level"      : "warning",
  "description": "close_notify"
}
)
javax.net.ssl|WARNING|53|reactor-http-nio-2|2022-10-12 02:11:22.252 CDT|SSLEngineOutputRecord.java:182|outbound has closed, ignore outbound application data
sjohnr commented 1 year ago

Thanks @jkjome. At this point, if you could at least share the your client code, mainly the entire controller and the Spring Security configuration, that would be helpful. I also think the log entries leading up to the stacktraces would be useful, but do make sure to set the log level to trace (logger.level.org.springframework.security=trace). The above seems to be missing much of the context around the request that triggers the error.

jkjome commented 1 year ago

Hi @sjohnr. See the attached zip file containing the code, as well as a text file containing relevant trace logging you requested: wcauthfail.zip

After further testing, it looks like there are 3 ingredients required to reproduce this issue...

  1. Use spring-security:6.0.0-M6+
  2. Set up spring-security config to enforce some sort of server authentication, e.g., BASIC Auth
  3. Host an API (protected in step 2) that, internally, uses WebClient to make an outbound call to a URL protected by Client-Cert Auth (and present the relevant certificate), and then stream that data back to the caller of the hosted API via DataBuffer/StreamingResponseBody

The zip file contains an application.properties file with some properties you'll need to provide values for, including keystore path, keystore password, and a test URL. There are a few API paths, including /hello, /fetch, and /stream. Two relevant APIs to look at in regard to the issue at hand are...

The /fetch path does NOT trigger the issue, even though it uses WebClient to make the outbound call. So, clearly, there is more to this than just using WebClient.

The /stream path DOES trigger the issue. The key difference to /fetch is that it responds with a DataBuffer/StreamingResponseBody. You'll notice in the log tracing that spring-security appears to apply authentication twice; once for the initial /stream call (expected), and then again during the attempted streaming of the response back to original caller of /stream (unexpected).

Hopefully that is all you will need to determine the root cause of this issue and prevent it from leaking into a non-milestone release.

sjohnr commented 1 year ago

Thanks @jkjome. After looking at the provided sample and creating a simple parallel example of my own, I believe I have partially reproduced it without Client-Cert auth.

I can reproduce an AccessDeniedException when returning a similar ResponseEntity<StreamingResponseBody> from the controller. The difference is that in my case, the response is already committed and so the 403 Forbidden does not show up in the response.

I believe the AccessDeniedException is related to Async integration in Spring Security (see Spring's documentation Asynchronous Requests for more info).

Specifically, Spring Security does support Callable return types, but does not support other async return types, such as DeferredResult (and I'm assuming StreamingResponseBody as well). Please note that this has been documented for some time in Spring Security and is not a new issue.

Prior to spring-security:6.0.0-M6, other dispatcher types were not affected by security rules. However, after gh-11492, other dispatcher types will be handled by the Spring Security filter chain, including ASYNC. But because the return type is not automatically handled by Spring Security's Async integration, your SecurityContext is lost and the async dispatch is considered unauthenticated/anonymous when it is processed by the filter chain (this shows up as a "second" request in your logs).

I'm not yet sure why your sample case is allowed write a 403 to the response, and why it appears to succeed in earlier releases (despite the AccessDeniedException), but I don't believe the overall issue here is new. It's also plausible that the workarounds you mentioned are actually viable in certain cases.

Short of adding an enhancement to support an expanded set of return types for Asynchronous request processing in Spring Security, I'm not sure this case would require fixing in time for the release (or indeed is even a bug, but an expected outcome in 6.0). I could be wrong, of course. We'll keep the issue open and revisit this after RC1 is released on Monday.

croudet commented 1 year ago

Hello, I am preparing for migration to spring-boot 3, I am using spring-security 6.0.0-SNAPSHOT.

I think I have the same issue, but with a controller, returning a ResponseEntity<StreamingResponseBody>. I have a custom BearerTokenResolver that also checks token in query parameter:

   @Bean
    BearerTokenResolver bearerTokenResolver() {
        final DefaultBearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver();
        bearerTokenResolver.setAllowUriQueryParameter(true);
        return bearerTokenResolver;
    }

When I am issuing a GET with a token in parameter, it is accepted but then an exception is thrown:

2022-11-17 09:03:35,698 ERROR [http-nio-8084-exec-1] - org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/recordings].[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] threw exception
org.springframework.security.access.AccessDeniedException: Access Denied
    at org.springframework.security.web.access.intercept.AuthorizationFilter.doFilter(AuthorizationFilter.java:98) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:183) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:172) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:133) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:126) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:120) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:183) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:172) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:133) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:91) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:85) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:183) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:172) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:133) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:100) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:183) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:172) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:133) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:179) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:183) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:172) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:133) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:183) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:172) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:133) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0.jar:6.0.0]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:183) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:172) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:133) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:183) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:172) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:133) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0.jar:6.0.0]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:183) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:172) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:133) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0.jar:6.0.0]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:183) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:172) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:133) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0.jar:6.0.0]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:183) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:172) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:133) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0.jar:6.0.0]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:183) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:172) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:133) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0.jar:6.0.0]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:183) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$AroundFilterObservation$SimpleAroundFilterObservation.lambda$wrap$0(ObservationFilterChainDecorator.java:275) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:169) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:133) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:233) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:351) ~[spring-web-6.0.0.jar:6.0.0]
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:267) ~[spring-web-6.0.0.jar:6.0.0]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.0.0.jar:6.0.0]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.0.jar:6.0.0]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0.jar:6.0.0]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.springframework.web.filter.ServerHttpObservationFilter.doFilterInternal(ServerHttpObservationFilter.java:108) ~[spring-web-6.0.0.jar:6.0.0]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.0.jar:6.0.0]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0.jar:6.0.0]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:691) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.ApplicationDispatcher.doDispatch(ApplicationDispatcher.java:612) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.ApplicationDispatcher.dispatch(ApplicationDispatcher.java:582) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.AsyncContextImpl$AsyncRunnable.run(AsyncContextImpl.java:588) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.AsyncContextImpl.doInternalDispatch(AsyncContextImpl.java:354) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:194) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:119) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:690) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:741) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.connector.CoyoteAdapter.asyncDispatch(CoyoteAdapter.java:247) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.coyote.AbstractProcessor.dispatch(AbstractProcessor.java:243) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:59) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:861) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1739) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at java.lang.Thread.run(Thread.java:833) ~[?:?]

2022-11-17 09:03:35,704 ERROR [http-nio-8084-exec-1] - org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/recordings].[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [/recordings] threw exception [Unable to handle the Spring Security Exception because the response is already committed.] with root cause
org.springframework.security.access.AccessDeniedException: Access Denied
    at org.springframework.security.web.access.intercept.AuthorizationFilter.doFilter(AuthorizationFilter.java:98) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:183) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:172) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:133) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:126) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:120) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:183) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:172) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:133) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:91) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:85) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:183) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:172) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:133) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:100) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:183) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:172) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:133) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:179) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:183) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:172) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:133) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:183) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:172) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:133) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0.jar:6.0.0]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:183) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:172) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:133) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:183) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:172) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:133) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0.jar:6.0.0]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:183) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:172) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:133) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0.jar:6.0.0]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:183) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:172) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:133) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0.jar:6.0.0]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:183) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:172) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:133) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0.jar:6.0.0]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:183) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:172) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:133) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0.jar:6.0.0]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:183) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$AroundFilterObservation$SimpleAroundFilterObservation.lambda$wrap$0(ObservationFilterChainDecorator.java:275) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:169) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:133) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:233) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191) ~[spring-security-web-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:351) ~[spring-web-6.0.0.jar:6.0.0]
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:267) ~[spring-web-6.0.0.jar:6.0.0]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.0.0.jar:6.0.0]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.0.jar:6.0.0]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0.jar:6.0.0]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.springframework.web.filter.ServerHttpObservationFilter.doFilterInternal(ServerHttpObservationFilter.java:108) ~[spring-web-6.0.0.jar:6.0.0]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.0.jar:6.0.0]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101) ~[spring-web-6.0.0.jar:6.0.0]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:691) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.ApplicationDispatcher.doDispatch(ApplicationDispatcher.java:612) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.ApplicationDispatcher.dispatch(ApplicationDispatcher.java:582) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.AsyncContextImpl$AsyncRunnable.run(AsyncContextImpl.java:588) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.AsyncContextImpl.doInternalDispatch(AsyncContextImpl.java:354) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:194) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:119) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:690) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:741) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.catalina.connector.CoyoteAdapter.asyncDispatch(CoyoteAdapter.java:247) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.coyote.AbstractProcessor.dispatch(AbstractProcessor.java:243) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:59) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:861) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1739) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-10.1.1.jar:10.1.1]
    at java.lang.Thread.run(Thread.java:833) ~[?:?]
2022-11-17 09:03:35,720 ERROR [http-nio-8084-exec-1] - org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration$StaticView - Cannot render error page for request [null] as the response has already been committed. As a result, the response may have the wrong status code.
2022-11-17 09:03:35,722 WARN  [http-nio-8084-exec-1] - org.apache.catalina.core.AsyncContextImpl - onError() call failed for listener of type [org.apache.catalina.core.AsyncListenerWrapper]

2022-11-17 09:03:35,722 WARN  [http-nio-8084-exec-1] - org.apache.catalina.core.AsyncContextImpl - onError() call failed for listener of type [org.apache.catalina.core.AsyncListenerWrapper]
java.lang.IllegalStateException: Cannot dispatch without an AsyncContext
    at org.springframework.util.Assert.state(Assert.java:76) ~[spring-core-6.0.0.jar:6.0.0]
    at org.springframework.web.context.request.async.StandardServletAsyncWebRequest.dispatch(StandardServletAsyncWebRequest.java:131) ~[spring-web-6.0.0.jar:6.0.0]
    at org.springframework.web.context.request.async.WebAsyncManager.setConcurrentResultAndDispatch(WebAsyncManager.java:400) ~[spring-web-6.0.0.jar:6.0.0]
    at org.springframework.web.context.request.async.WebAsyncManager.lambda$startCallableProcessing$2(WebAsyncManager.java:322) ~[spring-web-6.0.0.jar:6.0.0]
    at org.springframework.web.context.request.async.StandardServletAsyncWebRequest.lambda$onError$0(StandardServletAsyncWebRequest.java:146) ~[spring-web-6.0.0.jar:6.0.0]
    at java.util.ArrayList.forEach(ArrayList.java:1511) ~[?:?]

The current workaround is to add .dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll():

 @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.cors(Customizer.withDefaults());

        // Disable csrf, this is a stateless API
        http.csrf().disable();

        // Disable cookies
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        // All the requests that target the access API are set up with a custom JWT
        // authentication
        http.authorizeHttpRequests(authorize -> authorize
                        .requestMatchers(apiConfig.getPermitAllWhitelist()).permitAll()
                        .dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll()
                        .anyRequest().authenticated())
                .oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer
                        .jwt(jwt -> jwt.decoder(jwtDecoder()))
                        .authenticationEntryPoint(this::onAuthenticationError));
        return http.build();
    }
marcusdacoregio commented 1 year ago

I believe that this is not related only to client-cert authentication. The problem is around the behavior of async processing (emphasis by me):

When a controller returns a DeferredResult, the Filter-Servlet chain is exited, and the Servlet container thread is released. Later, when the DeferredResult is set, an ASYNC dispatch (to the same URL) is made, ...

When the Filter chain is exited, Spring Security saves the SecurityContext to the RequestAttributeSecurityContextRepository by default, actually called by DelegatingSecurityContextRepository. When the ASYNC dispatch is made, the SecurityContextHolderFilter should load the SecurityContext from the request attributes, but, the filter does not override OncePerRequestFilter#shouldNotFilterAsyncDispatch and, therefore, does not apply to that ASYNC dispatch, resulting in the SecurityContext not loaded into the current thread.

To exemplify better, here is my view of what is happening:

sequenceDiagram
    participant User
    participant Server
    participant ServerX509
    User->>Server: REQUEST /stream
    activate Server
    Server->>ServerX509: REQUEST /x509-protected
    activate ServerX509
    ServerX509-->>Server: 
    deactivate ServerX509
    Note over Server: Saves SecurityContext
    Server-->>User: 
    deactivate Server
    Server->>Server: ASYNC /stream
    Note over Server: No SecurityContext
  1. User performs a REQUEST to /stream
  2. The Server setup WebClient with Client Cert credentials and REQUEST /x509-protected
  3. The Server returns the REQUEST /stream to the client and saves the SecurityContext to RequestAttributeSecurityContextRepository
  4. The Server performs an ASYNC dispatch when the response is written, but there is no SecurityContext available, resulting in 401 #11027

@jkjome and @croudet, since this is a complex scenario to simulate, can you folks try something for me?

Create this filter:

public class MyFilter extends SecurityContextHolderFilter {

    public MyFilter() {
        super(new DelegatingSecurityContextRepository(new RequestAttributeSecurityContextRepository(), new HttpSessionSecurityContextRepository()));
    }

    @Override
    protected boolean shouldNotFilterAsyncDispatch() {
        return false;
    }
}

and register it in your HttpSecurity:

http.addFilterBefore(new MyFilter(), SecurityContextHolderFilter.class);

Please, let me know the outcome of this test so we can proceed with a fix. Thanks.

croudet commented 1 year ago

@marcusdacoregio So with the filter I no longer have the exception.

jkjome commented 1 year ago

@marcusdacoregio I tried the filter as well, and it works for me too.

schnapster commented 1 year ago

I'm not sure this is really fixed on v5.8.2 in Servlets. I've been trying to enable as much of the v6 in v5 behavior as possible, so my code looks something like

.authorizeHttpRequests(auth -> auth
    .shouldFilterAllDispatcherTypes(true) // the future v6 default
    //.dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll() // uncommenting this line fixes it
    .anyRequest().hasRole(role)
)

This breaks Controller methods that use e.g. CompletableFuture, returning a 401 despite the request having been executed.

I'm not married to v5.8 and intend to upgrade to v6 right afterwards, so I'm happy to just add .dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll() to fix it immediately, but thought I'd let you know that the v5.8 upgrade did not 100% work for me when enabling the v6 defaults.

marcusdacoregio commented 1 year ago

Hi @napstr,

Are you using requireExplicitSave(true)? Additionally, were you able to debug the SecurityContextHolderFilter and check if it's trying to load the SecurityContext? It is also important to verify what implementation of SecurityContextRepository you are using.

With that said, if you can confirm that this is not fixed and provide a minimal sample where we could go directly to the problem I recommend that you open a new issue and provide all the details there.

sjohnr commented 1 year ago

@napstr, in addition to requireExplicitSave(true) make sure you specify DelegatingSecurityContextRepository:

http
    // ...
    .securityContext((securityContext) -> securityContext
        .requireExplicitSave(true)
        .securityContextRepository(new DelegatingSecurityContextRepository(
            new RequestAttributeSecurityContextRepository(),
            new HttpSessionSecurityContextRepository()
        ))
    );
schnapster commented 1 year ago

Apologies for the late response. I tried specifying the DelegatingSecurityContextRepository but to no effect afaik. In the end, when we updated to v6 shortly after, I could remove the .dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll() workaround.

fkbeys commented 11 months ago

Although I applied all the solutions on the internet, I still could not solve the problem.

Synchronous method in my controller works normally. But the asynchronous method gives an authentication error every time. However, both methods do the same job and get the same Token. The codes of my project are available below. I think the bug is still not resolved.

https://github.com/fkbeys/javasprinasyncauthproblem

marcusdacoregio commented 11 months ago

Hi @fkbeys, it would be better if you could provide a minimal, reproducible sample. But I guess that you forget to save the SecurityContext into the SecurityContextRepository when you authenticate users, see the migration docs.

Since it looks like you are not using HTTP sessions, you might want to use the RequestAttributeSecurityContextRepository.

fkbeys commented 11 months ago

Hi @fkbeys, it would be better if you could provide a minimal, reproducible sample. But I guess that you forget to save the SecurityContext into the SecurityContextRepository when you authenticate users, see the migration docs.

Since it looks like you are not using HTTP sessions, you might want to use the RequestAttributeSecurityContextRepository.

Thank you for your help. I added the below code under the doFilterInternal


  RequestAttributeSecurityContextRepository requestAttributeSecurityContextRepository = new RequestAttributeSecurityContextRepository();
                SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
                securityContext.setAuthentication(authentication);
                requestAttributeSecurityContextRepository.saveContext(securityContext, request, response);

and also, in filter chain, i set the requireExplicitSave option as true. finally it worked. thank you.

kotharikrati commented 9 months ago

Hi @fkbeys Could you please add detailed solution how you fixed this : @marcusdacoregio : Could you please help here.

I am using it like this but could not fix the issue : @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (SecurityContextHolder.getContext().getAuthentication() != null && SecurityContextHolder.getContext().getAuthentication().isAuthenticated()) {

        SecurityContext context = SecurityContextHolder.getContext();

        this.repository.saveContext(context,request,response);
    }
   filterChain.doFilter(request, response);
}

and SecurityFilterChain looks like this:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http.cors().disable().authorizeHttpRequests()
            .requestMatchers("/configuration/ui",
                    "/swagger-resources/**",
                    "/configuration/security",
                    "/swagger-ui.html",
                    "/webjars/**",
                    "/actuator/**").permitAll().anyRequest().authenticated().and().headers((headers -> headers.withObjectPostProcessor(new ObjectPostProcessor<HeaderWriterFilter>() {
                @Override
                public HeaderWriterFilter postProcess(HeaderWriterFilter headerWriterFilter){
                    headerWriterFilter.setShouldWriteHeadersEagerly(true);
                    return headerWriterFilter;
                }
            }))).headers().frameOptions().sameOrigin().and()
            .oauth2ResourceServer(oauth2 -> oauth2
                    .authenticationManagerResolver(authenticationManagerResolver))

// .securityContext((securityContext -> securityContext.securityContextRepository(new RequestAttributeSecurityContextRepository()))) .build(); }

fkbeys commented 9 months ago

Hi @fkbeys Could you please add detailed solution how you fixed this : @marcusdacoregio : Could you please help here.

I am using it like this but could not fix the issue : @OverRide protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (SecurityContextHolder.getContext().getAuthentication() != null && SecurityContextHolder.getContext().getAuthentication().isAuthenticated()) {

        SecurityContext context = SecurityContextHolder.getContext();

        this.repository.saveContext(context,request,response);
    }
   filterChain.doFilter(request, response);
}

and SecurityFilterChain looks like this:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  return http.cors().disable().authorizeHttpRequests()
          .requestMatchers("/configuration/ui",
                  "/swagger-resources/**",
                  "/configuration/security",
                  "/swagger-ui.html",
                  "/webjars/**",
                  "/actuator/**").permitAll().anyRequest().authenticated().and().headers((headers -> headers.withObjectPostProcessor(new ObjectPostProcessor<HeaderWriterFilter>() {
              @Override
              public HeaderWriterFilter postProcess(HeaderWriterFilter headerWriterFilter){
                  headerWriterFilter.setShouldWriteHeadersEagerly(true);
                  return headerWriterFilter;
              }
          }))).headers().frameOptions().sameOrigin().and()
          .oauth2ResourceServer(oauth2 -> oauth2
                  .authenticationManagerResolver(authenticationManagerResolver))

// .securityContext((securityContext -> securityContext.securityContextRepository(new RequestAttributeSecurityContextRepository()))) .build(); } AuthEntryPointJwt.txt AuthTokenFilter.txt JwtUtils.txt WebSecurityConfig.txt

I have added 4 txt files for the authentication. they are actually .java files.

kotharikrati commented 9 months ago

Thank you @fkbeys for the detailed solution of yours.

kotharikrati commented 9 months ago

I believe that this is not related only to client-cert authentication. The problem is around the behavior of async processing (emphasis by me):

When a controller returns a DeferredResult, the Filter-Servlet chain is exited, and the Servlet container thread is released. Later, when the DeferredResult is set, an ASYNC dispatch (to the same URL) is made, ...

When the Filter chain is exited, Spring Security saves the SecurityContext to the RequestAttributeSecurityContextRepository by default, actually called by DelegatingSecurityContextRepository. When the ASYNC dispatch is made, the SecurityContextHolderFilter should load the SecurityContext from the request attributes, but, the filter does not override OncePerRequestFilter#shouldNotFilterAsyncDispatch and, therefore, does not apply to that ASYNC dispatch, resulting in the SecurityContext not loaded into the current thread.

To exemplify better, here is my view of what is happening:

sequenceDiagram
    participant User
    participant Server
    participant ServerX509
    User->>Server: REQUEST /stream
    activate Server
    Server->>ServerX509: REQUEST /x509-protected
    activate ServerX509
    ServerX509-->>Server: 
    deactivate ServerX509
    Note over Server: Saves SecurityContext
    Server-->>User: 
    deactivate Server
    Server->>Server: ASYNC /stream
    Note over Server: No SecurityContext
  1. User performs a REQUEST to /stream
  2. The Server setup WebClient with Client Cert credentials and REQUEST /x509-protected
  3. The Server returns the REQUEST /stream to the client and saves the SecurityContext to RequestAttributeSecurityContextRepository
  4. The Server performs an ASYNC dispatch when the response is written, but there is no SecurityContext available, resulting in 401 Authorization on Every Dispatch Type #11027

@jkjome and @croudet, since this is a complex scenario to simulate, can you folks try something for me?

Create this filter:

public class MyFilter extends SecurityContextHolderFilter {

  public MyFilter() {
      super(new DelegatingSecurityContextRepository(new RequestAttributeSecurityContextRepository(), new HttpSessionSecurityContextRepository()));
  }

  @Override
  protected boolean shouldNotFilterAsyncDispatch() {
      return false;
  }
}

and register it in your HttpSecurity:

http.addFilterBefore(new MyFilter(), SecurityContextHolderFilter.class);

Please, let me know the outcome of this test so we can proceed with a fix. Thanks.

Hi @marcusdacoregio

For Spring Security 6.1.2 , I cannot override the below method:

  @Override
protected boolean shouldNotFilterAsyncDispatch() {
    return false;
}

its no longer there in SecurityContextHolderFilter

kotharikrati commented 9 months ago
SecurityContextHolderFilter

Hi @marcusdacoregio , the overridden method : @Override protected boolean shouldNotFilterAsyncDispatch() { return false; }

only works for spring 3.0.0 and not for 3.0.6 or 3.0.1. because with Spring Security 6.0.1 onwards , SecurityContextHolderFilter doest not extends OncePerRequestFilter.

How can I fix the issue in this case? Can we have another fix for higher versions?

marcusdacoregio commented 9 months ago

Hi @kotharikrati, you should not need the workaround anymore if you are using the latest version of Spring Security or at least >= 5.8.1 or >= 6.0.1. If you have the same error it is likely another thing is happening, please add logging.level.org.springframework.security=TRACE to your application.properties and check what is happening.

kotharikrati commented 9 months ago

Hi @marcusdacoregio , I will check again . Thank you!

ppusda commented 2 months ago

If anyone is experiencing a similar problem, it may be helpful to check the document below.

https://docs.spring.io/spring-security/reference/5.8/migration/servlet/session-management.html https://docs.spring.io/spring-security/reference/servlet/authentication/session-management.html