1
votes

I have a requirement that is to expire a session after 30 minutes of inactivity.

My stack is the following:

  • JBoss 7.2
  • Spring MVC 4.0.6
  • Spring Security 3.2.4

Some relevant information:

  • Pre-authentication is done by JBoss (LDAP and SPNEGO)
  • Spring Security is used across the entire application for authorization purposes. SessionRegistry is working properly as I got a current HTTP sessions list (CurrentSessionController.java and a JSP) with the ability to expire existing sessions.
  • Java Config is preferred

The problem is whatever if a user idles for a while and then reach the session-timeout threshold defined in web.xml, he can still browse the application. A new HttpSession is created and he can still use the application.

What I would like is as soon as the session expires, user cannot call another request handler (ideally Spring Security would expire the session like when using sessionInformation.expireNow()). The user's next action (HTTP request) would redirect to a specific JSP page.

web.xml (1 minute for testing purpose)

<session-config>
    <session-timeout>1</session-timeout>
    <tracking-mode>COOKIE</tracking-mode>
</session-config>

AppInitializer.java

@Override
    public void onStartup(ServletContext servletContext) throws ServletException {

        WebApplicationContext applicationContext = this.getContext();

        this.setServletFilters(servletContext);
        this.setServletListeners(servletContext, applicationContext);

        ServletRegistration.Dynamic dispatcherServlet = servletContext.addServlet("dispatcherServlet", new DispatcherServlet(applicationContext));
        dispatcherServlet.setLoadOnStartup(1);
        dispatcherServlet.addMapping("/rest/*");
    }

    private AnnotationConfigWebApplicationContext getContext() {

        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        context.scan("my.app.spring4base.config");

        return context;
    }

    private void setServletFilters(ServletContext servletContext) {

        FilterRegistration.Dynamic springSecurityFilterChain = servletContext.addFilter("springSecurityFilterChain", new DelegatingFilterProxy("springSecurityFilterChain"));
        springSecurityFilterChain.addMappingForUrlPatterns(null, false, "/rest/*");

        FilterRegistration.Dynamic sessionFilter = servletContext.addFilter("sessionFilter", SessionFilter.class);
        sessionFilter.addMappingForUrlPatterns(null, false, "/rest/*");

        FilterRegistration.Dynamic requestContextFilter = servletContext.addFilter("requestContextFilter", RequestContextFilter.class);
        requestContextFilter.addMappingForUrlPatterns(null, false, "/*");

        FilterRegistration.Dynamic encodingFilter = servletContext.addFilter("encodingFilter", CharacterEncodingFilter.class);
        encodingFilter.addMappingForUrlPatterns(null, false, "/*");

        encodingFilter.setInitParameters(new HashMap<String, String>() {{
            put("encoding", StandardCharsets.UTF_8.name());
            put("forceEncoding", "true");
        }});
    }

    private void setServletListeners(ServletContext servletContext, WebApplicationContext applicationContext) {
        servletContext.addListener(new ContextLoaderListener(applicationContext));
        servletContext.addListener(new RequestContextListener());
        servletContext.addListener(new SpringApplicationScopedBeanDeprefixer());
        servletContext.addListener(new SpringSessionScopedBeanDeprefixer());
        servletContext.addListener(new HttpSessionEventPublisher());
    }

SecurityConfig.java

@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Bean
public CustomPreAuthenticationFilter customPreAuthenticationFilter() {
    return new CustomPreAuthenticationFilter();
}

@Bean
public SessionRegistry sessionRegistry() {
    return new SessionRegistryImpl();
}

@Override
protected void configure(HttpSecurity http) throws Exception {

    http

        .addFilterAfter(this.customPreAuthenticationFilter(), J2eePreAuthenticatedProcessingFilter.class)

        .csrf().disable()

        .logout()
            .logoutUrl("/rest/logout")
            .logoutSuccessUrl("/static/jsp/logout.jsp")
            .invalidateHttpSession(true)
            .deleteCookies("JSESSIONID")
            .permitAll()

        .and()

            .authorizeRequests()
            .antMatchers("/login","/accessDenied", "/sessionTimeout", "/resources/**", "/static/**").permitAll()
            .antMatchers("/security/*").hasRole("ADMIN")
            .antMatchers("/rest/**").authenticated()
            .antMatchers("/rest/**").hasRole("USER")
            .and().anonymous().disable()

         .jee()
            .mappableRoles("USER","ADMIN", "DEVELOPER")

         .and()
            .sessionManagement()
                .maximumSessions(1)
                .sessionRegistry(sessionRegistry())
                .maxSessionsPreventsLogin(true)
                .expiredUrl("/static/jsp/sessionTimeout.jsp")

            .and()
                .invalidSessionUrl("/static/jsp/sessionInvalid.jsp")
                .sessionFixation();
}

}

I am also using a Spring session scoped target bean (Session Object pattern), defined like so:

SessionObject.java

@Component
@Scope( 
    value = "session", 
    proxyMode = ScopedProxyMode.TARGET_CLASS
)
public class SessionObject {

    private User currentUser;

    public SessionObject() {
    }

    public User getCurrentUser() {
        return currentUser;
    }

    public void setCurrentUser(User user) {
        this.currentUser = user;
    }

    public boolean isConnected() {
        return currentUser != null; 
    }
}

I have tried using a Servlet Filter and then check if my Spring session scoped bean is present, but it turns out that it is always available.

SessionFilter.java

 public class SessionFilter implements Filter {

        public void destroy() {
        }

        @Override
        public void init(FilterConfig arg0) throws ServletException {
        }

    public void doFilter(ServletRequest req, ServletResponse res,
            FilterChain chain) throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        HttpSession session = request.getSession(false);

        //Also tried the following:
        //if (sessionObject == null) {
        if (session.getAttribute("scopedTarget.sessionObject") == null) {
            response.sendRedirect("/static/jsp/sessionTimeout.jsp");
        } else {
            chain.doFilter(request, response);
        }
    }
}

Logs from Spring Security when session times out

  • Session ID before timeout was: DnfCin+LXESn6QvKqd6jOlPe
  • Session ID after timeout (refresh) is: dcW8VDH7uBjmd9Ve4v4PoFHZ

Upon session timeout

2015-09-17 08:32:56,586 DEBUG [org.springframework.security.web.session.HttpSessionEventPublisher] (http-/0.0.0.0:8080-2) Publishing event: org.springframework.security.web.session.HttpSessionDestroyedEvent[source=org.apache.catalina.session.StandardSessionFacade@61da4a]
2015-09-17 08:32:56,586 DEBUG [org.springframework.security.core.session.SessionRegistryImpl] (http-/0.0.0.0:8080-2) Removing session DnfCin+LXESn6QvKqd6jOlPe from principal's set of registered sessions
2015-09-17 08:32:56,586 DEBUG [org.springframework.security.core.session.SessionRegistryImpl] (http-/0.0.0.0:8080-2) Removing principal org.springframework.security.core.userdetails.User@4eb878aa: Username: [email protected]; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_ADMIN,ROLE_DEVELOPER,ROLE_USER from registry

After session timeout (when new session is created)

2015-09-17 08:32:59,958 DEBUG [org.springframework.security.web.session.HttpSessionEventPublisher] (http-/0.0.0.0:8080-2) Publishing event: org.springframework.security.web.session.HttpSessionCreatedEvent[source=org.apache.catalina.session.StandardSessionFacade@276f3c]
2015-09-17 08:32:59,958 DEBUG [org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy] (http-/0.0.0.0:8080-2) Started new session: dcW8VDH7uBjmd9Ve4v4PoFHZ
2015-09-17 08:32:59,958 DEBUG [org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy] (http-/0.0.0.0:8080-2) Delegating to org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy@708afe
2015-09-17 08:32:59,958 DEBUG [org.springframework.security.core.session.SessionRegistryImpl] (http-/0.0.0.0:8080-2) Registering session dcW8VDH7uBjmd9Ve4v4PoFHZ, for principal org.springframework.security.core.userdetails.User@4eb878aa: Username: [email protected]; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_ADMIN,ROLE_DEVELOPER,ROLE_USER
2015-09-17 08:32:59,958 DEBUG [org.springframework.security.web.context.HttpSessionSecurityContextRepository] (http-/0.0.0.0:8080-2) SecurityContext stored to HttpSession: 'org.springframework.security.core.context.SecurityContextImpl@139f29a9: Authentication: org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken@139f29a9: Principal: org.springframework.security.core.userdetails.User@4eb878aa: Username: [email protected]; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_ADMIN,ROLE_DEVELOPER,ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.preauth.PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails@380f4: RemoteIpAddress: 127.0.0.1; SessionId: OUp-h8jL7jI0wIfGUEB9mgX8; [ROLE_ADMIN, ROLE_USER, ROLE_DEVELOPER]; Granted Authorities: ROLE_ADMIN, ROLE_DEVELOPER, ROLE_USER'
2015-09-17 08:32:59,958 DEBUG [org.springframework.security.web.FilterChainProxy] (http-/0.0.0.0:8080-2) /rest/welcome at position 11 of 12 in additional filter chain; firing Filter: 'ExceptionTranslationFilter'
2015-09-17 08:32:59,958 DEBUG [org.springframework.security.web.FilterChainProxy] (http-/0.0.0.0:8080-2) /rest/welcome at position 12 of 12 in additional filter chain; firing Filter: 'FilterSecurityInterceptor'
2015-09-17 08:32:59,958 DEBUG [org.springframework.security.web.util.matcher.AntPathRequestMatcher] (http-/0.0.0.0:8080-2) Checking match of request : '/rest/welcome'; against '/rest/logout'
2015-09-17 08:32:59,958 DEBUG [org.springframework.security.web.util.matcher.AntPathRequestMatcher] (http-/0.0.0.0:8080-2) Checking match of request : '/rest/welcome'; against '/login'
2015-09-17 08:32:59,958 DEBUG [org.springframework.security.web.util.matcher.AntPathRequestMatcher] (http-/0.0.0.0:8080-2) Checking match of request : '/rest/welcome'; against '/accessdenied'
2015-09-17 08:32:59,958 DEBUG [org.springframework.security.web.util.matcher.AntPathRequestMatcher] (http-/0.0.0.0:8080-2) Checking match of request : '/rest/welcome'; against '/sessiontimeout'
2015-09-17 08:32:59,958 DEBUG [org.springframework.security.web.util.matcher.AntPathRequestMatcher] (http-/0.0.0.0:8080-2) Checking match of request : '/rest/welcome'; against '/resources/**'
2015-09-17 08:32:59,958 DEBUG [org.springframework.security.web.util.matcher.AntPathRequestMatcher] (http-/0.0.0.0:8080-2) Checking match of request : '/rest/welcome'; against '/static/**'
2015-09-17 08:32:59,958 DEBUG [org.springframework.security.web.util.matcher.AntPathRequestMatcher] (http-/0.0.0.0:8080-2) Checking match of request : '/rest/welcome'; against '/security/*'
2015-09-17 08:32:59,958 DEBUG [org.springframework.security.web.util.matcher.AntPathRequestMatcher] (http-/0.0.0.0:8080-2) Checking match of request : '/rest/welcome'; against '/rest/**'
2015-09-17 08:32:59,958 DEBUG [org.springframework.security.web.access.intercept.FilterSecurityInterceptor] (http-/0.0.0.0:8080-2) Secure object: FilterInvocation: URL: /rest/welcome; Attributes: [hasRole('ROLE_USER')]
2015-09-17 08:32:59,958 DEBUG [org.springframework.security.web.access.intercept.FilterSecurityInterceptor] (http-/0.0.0.0:8080-2) Previously Authenticated: org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken@139f29a9: Principal: org.springframework.security.core.userdetails.User@4eb878aa: Username: [email protected]; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_ADMIN,ROLE_DEVELOPER,ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.preauth.PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails@380f4: RemoteIpAddress: 127.0.0.1; SessionId: OUp-h8jL7jI0wIfGUEB9mgX8; [ROLE_ADMIN, ROLE_USER, ROLE_DEVELOPER]; Granted Authorities: ROLE_ADMIN, ROLE_DEVELOPER, ROLE_USER
2015-09-17 08:32:59,958 DEBUG [org.springframework.security.access.vote.AffirmativeBased] (http-/0.0.0.0:8080-2) Voter: org.springframework.security.web.access.expression.WebExpressionVoter@1f8c768, returned: 1
2015-09-17 08:32:59,958 DEBUG [org.springframework.security.web.access.intercept.FilterSecurityInterceptor] (http-/0.0.0.0:8080-2) Authorization successful
2015-09-17 08:32:59,958 DEBUG [org.springframework.security.web.access.intercept.FilterSecurityInterceptor] (http-/0.0.0.0:8080-2) RunAsManager did not change Authentication object
2015-09-17 08:32:59,958 DEBUG [org.springframework.security.web.FilterChainProxy] (http-/0.0.0.0:8080-2) /rest/welcome reached end of additional filter chain; proceeding with original chain
2015-09-17 08:32:59,958 DEBUG [org.springframework.web.filter.RequestContextFilter] (http-/0.0.0.0:8080-2) Bound request context to thread: SecurityContextHolderAwareRequestWrapper[ org.springframework.security.web.context.HttpSessionSecurityContextRepository$Servlet3SaveToSessionRequestWrapper@f7ef18]
2015-09-17 08:32:59,958 DEBUG [org.springframework.web.servlet.DispatcherServlet] (http-/0.0.0.0:8080-2) DispatcherServlet with name 'dispatcherServlet' processing GET request for [/spring4base/rest/welcome]
2015-09-17 08:32:59,958 DEBUG [org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping] (http-/0.0.0.0:8080-2) Looking up handler method for path /welcome
2015-09-17 08:32:59,958 DEBUG [org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping] (http-/0.0.0.0:8080-2) Returning handler method [public java.lang.String my.app.spring4base.web.controller.WelcomeController.welcomePage()]
2015-09-17 08:32:59,958 DEBUG [org.springframework.beans.factory.support.DefaultListableBeanFactory] (http-/0.0.0.0:8080-2) Returning cached instance of singleton bean 'welcomeController'

Any help would be much appreciated.

Thank you

1
I would say that the problem comes from the fact that you are using a CustomPreAuthenticationFilter and possible the J2eePreAuthenticatedProcessingFilter as that basically will reauthenticate the user (I suspect). Checking in your SessionFilter will fail, especially in your case as it is defined after the Spring Security filters (so it will be present). Expiring the HttpSession is not possible from other threads/requests as that would basically be a security violation (how would you feel is someone could access your session state!). - M. Deinum
Try putting org.springframework.security to debug level logging - Spring security has normally very good logging and events like the one you are suspecting ( session invalidate and recreation) has high chances of turning up in the logs. - Shailendra
@Shailendra: I have added the logs to the question. Thank you! - Charles Morin
Not sure what your CustomPreAuthenticationFilter does but maybe you want to make it a bit smarter (or actually the J2eePreAuthenticatedProcessingFilter) you could make it a little smarter to not authenticate when an invalid session is detected but not sure what breaks in that case. - M. Deinum
As J2eePreAuthenticatedProcessingFilter is reauthenticating the user and filling the principal and the session fixation or session filter thinks that no session invalidation happened as there is new principal and blindly creates new session. So may be you can try to update user as expired while session invalidation and while pre-authentication check that if user is expired, no longer allow to authenticate..(Something like persistent_login idea to reauthenticate even when there is remember cookie after session timeout) - Anudeep Gade

1 Answers

0
votes

Above comment converted as one suggestion

As J2eePreAuthenticatedProcessingFilter is reauthenticating the user and filling the principal and the session fixation or session filter thinks that no session invalidation happened as there is new principal and blindly creates new session. So may be you can try to update user as expired while session invalidation and while pre-authentication check that if user is expired, no longer allow to authenticate..(Something like persistent_login idea to reauthenticate even when there is remember cookie after session timeout)