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
CustomPreAuthenticationFilterand possible theJ2eePreAuthenticatedProcessingFilteras that basically will reauthenticate the user (I suspect). Checking in yourSessionFilterwill fail, especially in your case as it is defined after the Spring Security filters (so it will be present). Expiring theHttpSessionis 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. DeinumCustomPreAuthenticationFilterdoes but maybe you want to make it a bit smarter (or actually theJ2eePreAuthenticatedProcessingFilter) 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