3

I am using Spring 3.2 Milestone 1 to implement a service with long polling. However for some reason Spring Security (3.1.2) clears the SPRING_SECURITY_CONTEXT immediately after the first deffered result either expires (asynctimeout has been reached and tomcat responds with http.200) or some response is send back to the client. Using Spring Security 3.1.0 this only happens under certain circumstances (HTTPS and client is behind some hardware firewall) but with 3.1.2 it happens always (after the first DefferedResult is fulfilled)!

Here is the debug output of the relevant part of the log

DEBUG: org.springframework.security.web.util.AntPathRequestMatcher - Checking match of request : '/updates/events'; against '/login*'
DEBUG: org.springframework.security.web.FilterChainProxy - /updates/events?clientId=nvrs1346481959144&timestamp=0&_=1346481959526 at position 1 of 11 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
DEBUG: org.springframework.security.web.context.HttpSessionSecurityContextRepository - Obtained a valid SecurityContext from SPRING_SECURITY_CONTEXT: 'org.springframework.security.core.context.SecurityContextImpl@fc783ee2: Authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@fc783ee2: Principal: org.springframework.security.core.userdetails.User@33ca09: Username: nvrs; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ADMIN; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@0: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: 46EC76439E921FE347EC48ECF71C1258; Granted Authorities: ADMIN'
DEBUG: org.springframework.security.web.FilterChainProxy - /updates/events?clientId=nvrs1346481959144&timestamp=0&_=1346481959526 at position 2 of 11 in additional filter chain; firing Filter: 'LogoutFilter'
DEBUG: org.springframework.security.web.FilterChainProxy - /updates/events?clientId=nvrs1346481959144&timestamp=0&_=1346481959526 at position 3 of 11 in additional filter chain; firing Filter: 'UsernamePasswordAuthenticationFilter'
DEBUG: org.springframework.security.web.FilterChainProxy - /updates/events?clientId=nvrs1346481959144&timestamp=0&_=1346481959526 at position 4 of 11 in additional filter chain; firing Filter: 'BasicAuthenticationFilter'
DEBUG: org.springframework.security.web.FilterChainProxy - /updates/events?clientId=nvrs1346481959144&timestamp=0&_=1346481959526 at position 5 of 11 in additional filter chain; firing Filter: 'RequestCacheAwareFilter'
DEBUG: org.springframework.security.web.FilterChainProxy - /updates/events?clientId=nvrs1346481959144&timestamp=0&_=1346481959526 at position 6 of 11 in additional filter chain; firing Filter: 'SecurityContextHolderAwareRequestFilter'
DEBUG: org.springframework.security.web.FilterChainProxy - /updates/events?clientId=nvrs1346481959144&timestamp=0&_=1346481959526 at position 7 of 11 in additional filter chain; firing Filter: 'RememberMeAuthenticationFilter'
DEBUG: org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter - SecurityContextHolder not populated with remember-me token, as it already contained: 'org.springframework.security.authentication.UsernamePasswordAuthenticationToken@fc783ee2: Principal: org.springframework.security.core.userdetails.User@33ca09: Username: nvrs; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ADMIN; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@0: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: 46EC76439E921FE347EC48ECF71C1258; Granted Authorities: ADMIN'
DEBUG: org.springframework.security.web.FilterChainProxy - /updates/events?clientId=nvrs1346481959144&timestamp=0&_=1346481959526 at position 8 of 11 in additional filter chain; firing Filter: 'AnonymousAuthenticationFilter'
DEBUG: org.springframework.security.web.authentication.AnonymousAuthenticationFilter - SecurityContextHolder not populated with anonymous token, as it already contained: 'org.springframework.security.authentication.UsernamePasswordAuthenticationToken@fc783ee2: Principal: org.springframework.security.core.userdetails.User@33ca09: Username: nvrs; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ADMIN; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@0: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: 46EC76439E921FE347EC48ECF71C1258; Granted Authorities: ADMIN'
DEBUG: org.springframework.security.web.FilterChainProxy - /updates/events?clientId=nvrs1346481959144&timestamp=0&_=1346481959526 at position 9 of 11 in additional filter chain; firing Filter: 'SessionManagementFilter'
DEBUG: org.springframework.security.web.FilterChainProxy - /updates/events?clientId=nvrs1346481959144&timestamp=0&_=1346481959526 at position 10 of 11 in additional filter chain; firing Filter: 'ExceptionTranslationFilter'
DEBUG: org.springframework.security.web.FilterChainProxy - /updates/events?clientId=nvrs1346481959144&timestamp=0&_=1346481959526 at position 11 of 11 in additional filter chain; firing Filter: 'FilterSecurityInterceptor'
DEBUG: org.springframework.security.web.util.AntPathRequestMatcher - Checking match of request : '/updates/events'; against '/updates/**'
DEBUG: org.springframework.security.web.access.intercept.FilterSecurityInterceptor - Secure object: FilterInvocation: URL: /updates/events?clientId=nvrs1346481959144&timestamp=0&_=1346481959526; Attributes: [hasAnyRole('ADMIN','MANAGER','INTERNAL')]
DEBUG: org.springframework.security.web.access.intercept.FilterSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@fc783ee2: Principal: org.springframework.security.core.userdetails.User@33ca09: Username: nvrs; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ADMIN; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@0: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: 46EC76439E921FE347EC48ECF71C1258; Granted Authorities: ADMIN
DEBUG: org.springframework.security.access.vote.AffirmativeBased - Voter: org.springframework.security.web.access.expression.WebExpressionVoter@52bf21bf, returned: 1
DEBUG: org.springframework.security.web.access.intercept.FilterSecurityInterceptor - Authorization successful
DEBUG: org.springframework.security.web.access.intercept.FilterSecurityInterceptor - RunAsManager did not change Authentication object
DEBUG: org.springframework.security.web.FilterChainProxy - /updates/events?clientId=nvrs1346481959144&timestamp=0&_=1346481959526 reached end of additional filter chain; proceeding with original chain
DEBUG: org.springframework.security.web.access.ExceptionTranslationFilter - Chain processed normally
DEBUG: org.springframework.security.web.context.SecurityContextPersistenceFilter - SecurityContextHolder now cleared, as request processing completed
DEBUG: org.springframework.security.web.access.ExceptionTranslationFilter - Chain processed normally
DEBUG: org.springframework.security.web.context.SecurityContextPersistenceFilter - SecurityContextHolder now cleared, as request processing completed
DEBUG: org.springframework.security.web.access.ExceptionTranslationFilter - Chain processed normally
DEBUG: org.springframework.security.web.context.SecurityContextPersistenceFilter - SecurityContextHolder now cleared, as request processing completed
DEBUG: org.springframework.security.web.util.AntPathRequestMatcher - Checking match of request : '/updates/events'; against '/login*'
DEBUG: org.springframework.security.web.util.AntPathRequestMatcher - Checking match of request : '/updates/events'; against '/resources/css/**'
DEBUG: org.springframework.security.web.util.AntPathRequestMatcher - Checking match of request : '/updates/events'; against '/resources/images/**'
DEBUG: org.springframework.security.web.util.AntPathRequestMatcher - Checking match of request : '/updates/events'; against '/resources/*'
DEBUG: org.springframework.security.web.FilterChainProxy - /updates/events?clientId=nvrs1346481959144&timestamp=0&_=1346481985081 at position 1 of 11 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
DEBUG: org.springframework.security.web.context.HttpSessionSecurityContextRepository - HttpSession returned null object for SPRING_SECURITY_CONTEXT
DEBUG: org.springframework.security.web.context.HttpSessionSecurityContextRepository - No SecurityContext was available from the HttpSession: org.apache.catalina.session.StandardSessionFacade@61ed10f7. A new one will be created.

If you look carefully at the output you will see the first long poll request "/updates/events?" is processed correctly - granted access but after that the spring security context gets cleared as you may see from the line "HttpSession returned null object for SPRING_SECURITY_CONTEXT" that is triggered by another request to that URL by the client after the first one has expired or an event triggers a non emty response. I would like to point out here that i all custom filters have been disabled and when processing the long poll request i store the DefferedResult to a Map with the sessionId-clientid (unique for each page instance browser tab) as a key for accessing it and sending a result to the client in case a JMS message is received.

The problem is present for Spring framework 3.2 M1 and the latest 3.2 snapshot build in combination with Spring Security 3.1.2 or its respective latest snapshot under Tomcat 7.0.28 / 7.0.29 (both default and APR connectors).

nvrs
  • 720
  • 2
  • 17
  • 25
  • There isn't really any evidence here of anything abnormal (nor in your answer below). A more likely explanation is that you are losing the session for some reason (e.g. because of a switch between HTTP/HTTPS) and hence getting a null context. You'll need to demonstrate that this isn't the case. – Shaun the Sheep Sep 03 '12 at 15:37
  • I am definetely not switching between HTTP and HTTPS. I ll try to make something more appropriate to show what i am talking about – nvrs Sep 03 '12 at 17:08

2 Answers2

3

With the debuggers help i have reached to the following conclusion:

After the DefferedResult is set the method flush() of org.springframework.security.web.context.SaveContextOnUpdateOrErrorResponseWrapper gets called which via a proxy calls saveContext() of org.springframework.security.web.context.HttpSessionSecurityContextRepository.

@Override
protected void saveContext(SecurityContext context) {
    final Authentication authentication = context.getAuthentication();
    HttpSession httpSession = request.getSession(false);

    // See SEC-776
    if (authentication == null || authenticationTrustResolver.isAnonymous(authentication)) {
        if (logger.isDebugEnabled()) {
            logger.debug("SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.");
        }

        if (httpSession != null && !contextObject.equals(contextBeforeExecution)) {
            // SEC-1587 A non-anonymous context may still be in the session
            // SEC-1735 remove if the contextBeforeExecution was not anonymous
            httpSession.removeAttribute(springSecurityContextKey);
        }
        return;
    }

Since the authentication object is null (due to the fact that the spring security context has been cleared) the line httpSession.removeAttribute(springSecurityContextKey) removes the SPRING_SECURITY_CONTEXT from the session and the next request that the user makes results in a session with no security context and thus user is redirected to login. Unless i am missing something obvious here, this is a deal breaker for async requests. I wonder if the Spring Security team is aware of the issue and if they plan to fix it before 3.2 gets released. In the meantime does anyone have any suggestions for a proper workaround?

edit: at the moment as a temporary solution i deal with the issue by not editing the session in the case of an async request. Specifically i modified the check when to flush the securityContext from:

if (httpSession != null && !contextObject.equals(contextBeforeExecution))

to

if (httpSession != null && !contextObject.equals(contextBeforeExecution) && this.request.getAttribute("javax.servlet.async.request_uri") == null)

Thanks

nvrs
  • 720
  • 2
  • 17
  • 25
  • You say "the authentication object is null". Why has the context been cleared during this request? Remember the security context is a thread-local object. This should probably be part of your question rather than an answer. – Shaun the Sheep Sep 03 '12 at 15:41
  • As you say, the security context is thread-local and as far as i can tell it does not get saved when the async request is saved. Therefore when the asyn context is resumed, the authentication object is null – nvrs Sep 03 '12 at 17:04
  • If you can clearly demonstrate the issue then it sounds like something that would probably be better dealt with in the Spring issue tracker rather than SO. I'd open an issue there. – Shaun the Sheep Sep 03 '12 at 19:45
  • Asynch support is not implemented in Spring Security yet. Follow [SEC-1998](https://jira.springsource.org/browse/SEC-1998) to see its progress. – Rob Winch Sep 03 '12 at 22:31
  • You should give Spring 3.2 + Spring Security 3.2 (M1) a try. Should be fixed. – knalli May 09 '13 at 10:42
  • I am facing the same problem, can u show the entire class which u have given in answer. Thank you. :-) – We are Borg Oct 12 '18 at 11:43
0

Are you using forked threads on the server side?

We had a similar problem of the security context being cleared because we had forked threads interacting with the response. The security context is a thread local variable and by default not shared with forked threads (see doc).

We fixed this issue by setting the strategy of SecurityContextHolder to MODE_INHERITABLETHREADLOCAL:

spring security config xml:

<beans:bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    <property name="targetClass" value="org.springframework.security.core.context.SecurityContextHolder" />
    <property name="targetMethod" value="setStrategyName" />
    <property name="arguments" value="MODE_INHERITABLETHREADLOCAL" />
</beans:bean>

UPDATE 2016-10-31:

Using INHERITABLETHREADLOCAL might cause ThreadLocal leaks in environments using Thread Pools (e.g. Tomcat). There are also other ways (e.g. use DelegatingSecurityContextRunnable) to make sure forked threads get the correct Spring security context. See Concurrency Support.

R. Oosterholt
  • 7,720
  • 2
  • 53
  • 77