Spring Security part VI : Session Timeout handling for Ajax calls

Recently when developing the Tatami application for the Twitter-like contest, I faced an annoying issue: how to detect an user session timeout when an Ajax request is triggered from the browser ?

If you’re not familiar yet with Spring Security, you can check my previous articles on this framework.

In this article we’ll see a solution based on Spring Security filter. For those who don’t use Spring Security I’ll show another approach with servlet filter.

Please note that a demo application for this article can be found on GitHub https://github.com/doanduyhai/AjaxSessionExpiration

Edit: the implementation has been changed to simplify Ajax request detection thanks to Marty Jones suggestions.
 

I The problem

Nowadays mobile development is getting more and more important for the business. Many companies provide a mobile interface of their traditional desktop website to address a larger audience.

The corner-stone of mobile architecture are RESTfull webservices. The underlying implementation relies on Ajax calls to achieve content lazy loading.

If your resources is protected by a security framework any un-authenticated request will be rejected and the framework redirects you to a login page. The same mechanism applies if the request is Ajax-based.

However, at the XMLHttpRequest level, it is not possible to detect this redirection. According to the W3C specs, the redirection is taken care at browser level and should be transparent for the user (so transparent for the XMLHttpRequest protocol too). What happens it that the Ajax layer receives an HTTP 200 code after the redirection.

This issue has been widely debated on Stackoverflow at this thead.
Many contributors suggest to add a special attribute in the JSON response to indicates a redirect or to add a special Javascript callback to parse the response string searching for a DOM element that characterizes the login page and so on.

My opinion is that all these solutions are not satisfactory because they require many hacks and tamper with the response string/content itself.
 

II The solution

A Spring Security

1) Algorithm

Since it is clear that it’s not possible to detect an HTTP Redirect at Javascript level, the job should be done at server side.

The idea is to add a special filter in the Spring Security chain to detect a session timeout and an incoming Ajax call then return an custom HTTP error code so the Ajax callback function can detect and process properly.

Below is a pseudo-code of the filter implementation:

  • If not authenticated
    • Redirect to login page
  • If the access to the resource is denied
    • If the session has expired and the request is Ajax-based
      • Return custom HTTP error code
    • Else
      • Redirect to login page
  • Else
    • Redirect to login page
2) Implementation

We should create a custom filter class implementing the org.springframework.web.filter.GenericFilterBean interface.

public class AjaxTimeoutRedirectFilter extends GenericFilterBean
{

	private static final Logger logger = LoggerFactory.getLogger(AjaxTimeoutRedirectFilter.class);

	private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();
	private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();

	private int customSessionExpiredErrorCode = 901;

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
	{
		try
		{
			chain.doFilter(request, response);

			logger.debug("Chain processed normally");
		}
		catch (IOException ex)
		{
			throw ex;
		}
		catch (Exception ex)
		{
			Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
			RuntimeException ase = (AuthenticationException) throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);

			if (ase == null)
			{
				ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
			}

			if (ase != null)
			{
				if (ase instanceof AuthenticationException)
				{
					throw ase;
				}
				else if (ase instanceof AccessDeniedException)
				{

					if (authenticationTrustResolver.isAnonymous(SecurityContextHolder.getContext().getAuthentication()))
					{
						logger.info("User session expired or not logged in yet");
						String ajaxHeader = ((HttpServletRequest) request).getHeader("X-Requested-With");

						if ("XMLHttpRequest".equals(ajaxHeader))
						{
							logger.info("Ajax call detected, send {} error code", this.customSessionExpiredErrorCode);
							HttpServletResponse resp = (HttpServletResponse) response;
							resp.sendError(this.customSessionExpiredErrorCode);
						}
						else
						{
							logger.info("Redirect to login page");
							throw ase;
						}
					}
					else
					{
						throw ase;
					}
				}
			}

		}
	}

	private static final class DefaultThrowableAnalyzer extends ThrowableAnalyzer
	{
		/**
		 * @see org.springframework.security.web.util.ThrowableAnalyzer#initExtractorMap()
		 */
		protected void initExtractorMap()
		{
			super.initExtractorMap();

			registerExtractor(ServletException.class, new ThrowableCauseExtractor()
			{
				public Throwable extractCause(Throwable throwable)
				{
					ThrowableAnalyzer.verifyThrowableHierarchy(throwable, ServletException.class);
					return ((ServletException) throwable).getRootCause();
				}
			});
		}

	}

	public void setCustomSessionExpiredErrorCode(int customSessionExpiredErrorCode)
	{
		this.customSessionExpiredErrorCode = customSessionExpiredErrorCode;
	}
}

This class is a copy of the org.springframework.security.web.access.ExceptionTranslationFilter class with some modifications to detect session timeout with Ajax-based request.

First we define 1 new attribute:

  • customSessionExpiredErrorCode: custom HTTP error code to return for session timeout with Ajax-based request. The default value is 901

The main job is done at lines 46 & 48. We compare the request header “X-Requested-With“, when it exists, with the value “XMLHttpRequest” to detect whether the current request is Ajax-based or not. Please note that I did the test with IE 9 only, not sure it works for older IE version.

In most cases we simply re-throw the exception except when the call is Ajax-based. In this case we force an HTTP error code and completely bypass the remaining filters in the security filter chain.

 

3) Filter configuration

The idea is to add the above custom filter in the Spring Security filter chain. The order in the filter chain is crucial. Our filter should intercept the session timeout for Ajax calls before the vanilla ExceptionTranslationFilter in order to send the custom HTTP error code.

Configuration for Spring Security namespace users:

...
<beans:bean id="ajaxTimeoutRedirectFilter" class="doan.ajaxsessionexpiration.demo.security.AjaxTimeoutRedirectFilter">
	<beans:property name="customSessionExpiredErrorCode" value="901"/>
</beans:bean>
...
 <http auto-config="true" use-expressions="true">
        <intercept-url pattern="/**" access="isAuthenticated()" />
        <custom-filter ref="ajaxTimeoutRedirectFilter" after="EXCEPTION_TRANSLATION_FILTER"/>
	...
	...
</http>

Please notice that at line 10 we define our custom filter which is placed AFTER the EXCEPTION_TRANSLATION_FILTER in the chain. Being put later in the chain means that our filter can catch security exceptions before the EXCEPTION_TRANSLATION_FILTER which is what we want.

Configuration without Spring Security namespace:

...
<bean id="ajaxTimeoutRedirectFilter" class="doan.ajaxsessionexpiration.demo.security.AjaxTimeoutRedirectFilter">
	<property name="customSessionExpiredErrorCode" value="901"/>
	<property name="restString" value="/rest"/>
	<property name="restStringPatternMode" value="PREFIX"/>
</bean>
...
<bean id="securityFilterChain" class="org.springframework.security.web.FilterChainProxy">
	<sec:filter-chain-map request-matcher="ant">
	    <sec:filter-chain pattern="/**"  filters="
	           securityContextPersistentFilter,
	           logoutFilter,
	           authenticationProcessingFilter,
	           anonymousFilter,
	           exceptionTranslationFilter,
	           ajaxTimeoutRedirectFilter,
	           filterSecurityInterceptor
	           " />
  	</sec:filter-chain-map>
</bean>

B Other security frameworks

If you do not use Spring Security, there is a generic solution though. The idea is to add a special Http filter and check for HTTP session validity. If the session is no longer valid (timeout) and if the request is Ajax-base, you send a custom HTTP error as above.

First you should add a custom filter in the web.xml file:

...
<filter>
	<filter-name>ajaxSessionExpirationFilter</filter-name>
	<filter-class>doan.ajaxsessionexpiration.demo.security.AjaxSessionExpirationFilter</filter-class>
	<init-param>
		<param-name>customSessionExpiredErrorCode</param-name>
		<param-value>901</param-value>
	</init-param>
</filter>
...
<filter-mapping>
    	<filter-name>ajaxSessionExpirationFilter</filter-name>
    	<url-pattern>/*</url-pattern>
</filter-mapping>

Please note that this filter should be the one to kick in first (the first in the filter chain).

Then the implementation:

public class AjaxSessionExpirationFilter implements Filter
{

	private int customSessionExpiredErrorCode = 901;

    	@Override
	public void init(FilterConfig arg0) throws ServletException
	{
		// Property check here
	}

    	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain filerChain) throws IOException, ServletException
	{
		HttpSession currentSession = ((HttpServletRequest)request).getSession(false);
		if(currentSession == null)
		{
			String ajaxHeader = ((HttpServletRequest) request).getHeader("X-Requested-With");
			if("XMLHttpRequest".equals(ajaxHeader))
			{
				logger.info("Ajax call detected, send {} error code", this.customSessionExpiredErrorCode);
				HttpServletResponse resp = (HttpServletResponse) response;
				resp.sendError(this.customSessionExpiredErrorCode);
			}
			else
			{
				// Redirect to login page
			}
		}
		else
		{
			// Redirect to login page
		}
	}
}

The session timeout detection is done at line 21. The getSession(false) method of the HttpServletRequest interface will return a HTTP session if one exists (and has not expired yet) but will not create a new one if no session exists or if the current session has expired.

The rest of the processing is similar to the Spring Security version.
 

C Client-side implementation

On the client side we should configure the Ajax callback function so it can handle properly our custom HTTP error code. For this jQuery comes to the rescue:

<script> 
function ajaxSessionTimeout()
{
	// Handle Ajax session timeout here
}

!function( $ )
{
	$.ajaxSetup({
		statusCode: 
		{
			901: ajaxSessionTimeout
		}
	});
}(window.jQuery);
</script>

We use the jQuery.ajaxSetup() function to define a custom behavior when an HTTP 901 status code is encountered. This is a mere callback to a function to handle Ajax request timout.

And that’s all !
 

III Demo application

I’ve created a demo application to illustrate this article. I set the session timeout to 1 minute.

On the main page, we have a popover element which trigger Ajax call to fetch user name from server.

ajaxsessionexpiration-popover

After 1 minute the session expires, the Ajax error callback for Ajax session timeout is called. In this case I display a modal panel informing the user that his session has expired and prompting him to go back to login page to re-authenticate.

48 Comments

  1. Not Relevant

    I see part IV, I see part VI. Where is part V ?

    Reply
    1. DuyHai DOAN

      Hello dear “Not Relevant” reader 😀

      Part V can be found here: http://doanduyhai.wordpress.com/category/spring/security/

      Indeed this article belongs to a serie dedicated to Spring Security.

      You can navigate to a particular serie with the menu (Spring/Security)

      Hope that it helped

      Reply
  2. Marty Jones

    I also had this same issue and I went a slightly different direction. I extended the LoginUrlAuthenticationEntryPoint class and overrode the commence() method. In my case I return a 401 return code but you could return whatever you like.

    Here is an example:

    public class CustomLoginUrlAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {

    public void commence(HttpServletRequest request,HttpServletResponse response,
    AuthenticationException authException) throws IOException, ServletException {

    if (HttpRequestUtil.isAjaxRequest(request) && authException != null) {
    response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    }
    else {
    super.commence(request, response, authException);
    }
    }
    }

    Reply
    1. DuyHai DOAN

      Thanks for sharing Marty!

      Your solution is great! But I cannot find the HttpRequestUtil package anywhere. Instead I found an implementation for AjaxUtils here:

      https://github.com/SpringSource/spring-mvc-showcase/blob/master/src/main/java/org/springframework/mvc/extensions/ajax/AjaxUtils.java

      They’re comparing request header against the string “XMLHttpRequest”. I don’t know if it would work for old IE versions since Microsoft uses ActiveXObject instead.

      Anyway great comment!

      Reply
      1. Marty Jones

        Sorry about that. Here is my HttpRequestUtil class:

        public static boolean isAjaxRequest(HttpServletRequest request) {
        return “XMLHttpRequest”.equalsIgnoreCase(request.getHeader(“X-Requested-With”));
        }

        Reply
  3. Leonid

    Thank you for a nice solution.
    A small notice (a just faced it in my project): if you have session-management and invalid-session-url set, the SessionManagementFilter will process the request before it gets to your filter (i.e. say “Session ID is invalid”).

    Reply
    1. DuyHai DOAN

      Hello Leonid

      You’re right. My solution is based on a classical authentication filter chain without SessionManagementFilter. I guess that in your case, you need to move the “Ajax Timeout detection” step before the session management filter.

      Reply
  4. Ajax-Jquey-Spring Security

    thanks a lot.

    I used the same approach but every time session time out occurs i am getting 500 as error code instead of 901.

    Reply
    1. DuyHai DOAN

      Try to do step by step debug in the AjaxTimeoutRedirectFilter class to see if your code steps into the if(“XMLHttpRequest”.equals(ajaxHeader)) block. You’re probably facing another exception that generates the defaut 500 HTTP error.

      Reply
  5. smratx

    In my project ,I use Struts2 +Spring 3+Hibernate 3+Spring Security 3.After adding the filter you give,the filter can’t detect the authenticationexception.The logger output always display DEBUG AjaxTimeoutRedirectFilter:40 – Chain processed normally.I’m sorry for my pool english.

    Reply
    1. smratx

      the error code is 302

      Reply
      1. smratx

        How can I catch authenticationexception in struts2 action, and so your custom spring security filter chain can detect it.

        Reply
      2. DuyHai DOAN

        If you’re catching an HTTP 302 code, it means that the request is being re-directed already. It also means that the filter is intercepting AFTER the timeout detection by Spring security chain. Can you provide your Spring security filter chain config ?

        Reply
      3. smratx
        Reply
  6. smratx

    DuyHai DOAN:Thanks a lot.My configuration is as follow:

    Reply
    1. smratx

      <http auto-config=”false” access-denied-page=”/accessDenied.jsp”>
      <session-management invalid-session-url=”/login/login.jsp” session-fixation-protection=”none”>
      <concurrency-control error-if-maximum-exceeded=”true” max-sessions=”10″/>
      </session-management>
      <intercept-url pattern=”/*.*” requires-channel=”https”/>
      <custom-filter ref=”ajaxTimeoutRedirectFilter” after=”EXCEPTION_TRANSLATION_FILTER”/>
      <form-login login-page=”/login/login.jsp”
      authentication-failure-url=”/login/login.jsp?error=true” default-target-url=”/systemInit/init.xhx”/>
      <logout logout-success-url=”/login/login.jsp” />
      </http>

      Reply
  7. smratx

    Thanks a lot . I delete the property invalid-session-url=”/login/login.jsp” from session-management, then the filter worked fine for me.I think the session-management filter is before AjaxTimeoutRedirectFilter,but this can not explain one thing that the 302 status code is only occurred after the first ajax request(The follow-up request returned the customed status code correctly.) I want to know why,who can tell me.I am very sorry once again for my pool english.

    Reply
    1. DuyHai DOAN

      smratx

      According to the official Spring Security documentation, the <session-management> tag is meant for session timeout handling.

      Of course if you put <session-management> in your config, it will add a filter in your chain so our ajaxTimeOutFilter is made useless …

      Reply
      1. smratx

        Delete the property invalid-session-url=”/login/login.jsp” from session-management, then the filter work fine.

        Reply
  8. Abdiel

    Hello, how can I recover the message sessionScope $ {[“SPRING_SECURITY_LAST_EXCEPTION.”]} Message, using th:text?

    Reply
    1. DuyHai DOAN

      Abdiel

      You can try:

      • th:text=”${#httpSession[‘SPRING_SECURITY_LAST_EXCEPTION’]}”
      • th:text=”${#session[‘SPRING_SECURITY_LAST_EXCEPTION’]}”
      • th:text=”${#session.getAttribute(‘SPRING_SECURITY_LAST_EXCEPTION’)}”
      Reply
      1. Abdiel

        Thanks man. I am studying the framework thymeleaf and am finding it very interesting. The code is clean, utlizando thymeleaf!

        Reply
      2. Abdiel

        I have another question. How would the behavior with thymeleaf, and am using tiles definition? Would that change the whole structure of the application.

        Reply
      3. DuyHai DOAN

        Abdiel, see my answer below

        Reply
  9. DuyHai DOAN

    Abdiel

    One of my collegue asked me the same question earlier

    Basically he is working with plain JSP tags (as you with Apache Tiles) and he wants to migrate to ThymeLeaf without breaking every thing.

    The idea is to use both templating systems during the migration process, each templating engine acts as a ViewResolver from Spring point of view.

    With Spring MVC you can have as many ViewResolvers as you want, specifying their order and defining the page prefix.

    	<bean id="contentNegotiatingResolver" class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
    		<property name="order" value="#{T(org.springframework.core.Ordered).HIGHEST_PRECEDENCE}" />
    		<property name="mediaTypes">
    			<map>
    				<entry key="html" value="text/html"/>
    				<entry key="pdf" value="application/pdf"/>
    				<entry key="xsl" value="application/vnd.ms-excel"/>
    				<entry key="xml" value="application/xml"/>
    				<entry key="json" value="application/json"/>
    			</map>
    		</property>
    	</bean>
    
    	<bean id="viewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver">
      		<property name="viewClass" value="org.springframework.web.servlet.view.tiles2.TilesView"/>
      		<property name="order" value="1" /> <!-- FIRST RESOLVER -->
      		<property name="suffix" value=".jsp"/> <!-- .jsp extensions for Apache Tiles files -->
    	</bean>
    
    	<bean class="org.thymeleaf.spring3.view.ThymeleafViewResolver"> 
    		<property name="templateEngine" ref="templateEngine" /> 
    		<property name="order" value="2" /> <!-- SECOND RESOLVER -->
    		<property name="suffix" value=".html" /> <!-- .html extension for ThymeLeaf files -->
    		<property name="characterEncoding" value="UTF-8"/>
    	</bean>
    
    Reply
    1. Abdiel

      DuyHai Doan, very cool. Would be perfect integration, along with Apache Tiles and Thymeleaf. I thought I might have a conflict using two viewresolvers.

      very thanks

      Reply
  10. Hannes

    Hi,

    when I am making the first call as ajax call and the second as form based call, the second call is still recognised as ajax call.

    Did you see this too?

    Thanks

    Reply
    1. DuyHai DOAN

      Hello Hannes

      I did not try your scenario. The simplest thing to do is to set a debug point in the Filter at:

      ...
      String ajaxHeader = ((HttpServletRequest) request).getHeader("X-Requested-With");
      ...
      

      and check to see whether in the form based call, the request header still returns “X-Requested-With”

      Reply
  11. Abdi

    Hi,

    The validation of the request X-Requested-With not. You must set the Content-Type? Because the browser does not appear to respond to the head X-Requested-With.

    thank you

    Reply
    1. DuyHai DOAN

      Hello Abdi

      The “X-Requested-With” header is normally set automatically. Maybe it’s something specific to JQuery and not to all Ajax request.

      To be honest, I only use JQuery for AJAX in my projects. I will give a try with manual Ajax request to double-check.

      Thanks for the remarks anyway

      Reply
      1. LouMĂ©ou

        Hi everybody,
        first of all, thanks you DuyHai for this great post, it helps me a lot.

        About Abdi issue, I don’t know if we faced the same,
        but I had to apply a few changes to ajax request detection function,
        as “X-Requested-With” header attribute was not present in my case.

        Looking at request parameter, I found AJAXREQUEST parameter.
        The root seems to come from richfaces ajax calls, as you ma read here
        https://community.jboss.org/thread/16614.

        Hope it will help.
        Regards,
        Nicolas

        Reply
    2. DuyHai DOAN

      I’ve checked and indeed the “X-Requested-With” header is sent by most of Ajax Javascript library. It’s not a standard though… http://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Common_non-standard_request_headers

      Reply
  12. Pham

    Salut anh Hai,
    je suis Vietnamienne comme toi.
    maintenance je suis en train d’integrer spring security 3.0.7 dans mon projet(Spring 3.0+hibernate) j’ai fait bien login avec security . mais j’ai quelques problems de security d’appel AJAX.
    je veux faire security pour tous les URL(appel AJAX) par ex: /http://localhost:8080/myprojet/deleteUser. c-t-d: quand un Utilisateur ai a role Admin il peut appeller ce URL si non il n’ai pas le role ADMIN , il ne peux pas appeller ce URL
    est-ce que vous pouvez me donner quelques idees ou quelque lien qui concerne ce problem?

    desolle si Spam.
    Merci bcp

    Reply
    1. DuyHai DOAN

      Salut Pham

      Tu peux regarder ce post en dĂ©tail, il explique comment configurer les restrictions d’URL en fonction des roles: http://doanduyhai.wordpress.com/2012/02/19/spring-security-part-iv-exceptiontranslationfilter-filtersecurityinterceptor

      Pour ton cas, je pense que tu devrais définit un pattern comme ceci:

      <property name="securityMetadataSource">
      		<sec:filter-security-metadata-source lowercase-comparisons="true" request-matcher="ant" use-expressions="true">
      			...
      			<sec:intercept-url pattern="/deleteUser" access="hasRole('ADMIN')"/>
      			...
      		</sec:filter-security-metadata-source>
      	</property>
      

      J’imagine que dans l’URL http://localhost:8080/myProject/deleteUser, myProject est ton context Path. Du coup il suffit de protĂ©ger la partie /deleteUser comme configurĂ© ci-dessus.

      Reply
  13. Ambika

    This works absolutely fantastic, Thank you so much for posting it.

    Reply
  14. Eshana

    Removed invalid-session-url and added expired-url. works like charm! 🙂

    Placement of this filter after exception translation filter is correct 🙂

    This will not work if we add this filter before session management filter in case we have configured invalid-session-url; as the default filter will forward the request to the url once the dofilter line in our custom filter gets executed.

    Reply
  15. Matteo

    Great post!
    However i found a little problem in your code for my needs. The code in section 2) does not rethrow the exception (if not “ase”) in any case, so i added the line “throw ex;” at line 66

    Reply
    1. DuyHai DOAN

      Good catch ! Thanks for your remark

      Reply
      1. Matteo

        U’re welcome. Actually i refine my own correction and the right solution was to put an else clause at that line :
        else {
        throw ex;
        }

        however i’m having problem to having it compatible with Java 6 cause it seems to break the
        contract of javax.Filter interface by throwing a generic Exeption ex.
        So i’m trying to work around it as follows:
        else {
        //verify if it works.
        //Java 7 version worked with a simple “throw ex;”
        throw new IOException(ex);
        }

        cause Filter class can throw an IOException.

        Reply
  16. will824

    Greetings DuyHai DOAN,
    Excellent alternative for capturing Session Timeouts in Ajax. Just wanted to thank you for sharing this information and the great article 🙂

    Reply
  17. registre des creations

    sympa mais ce soir” il faut “vous dĂ©cider mon site registre des creations

    Reply
  18. Jun Wang

    This is actually a bit of issue if the application server sits behind reverse proxy (eg Apache). The reverse proxy doesn’t understand error code 901 and will just give a 500 error which doesn’t recognized by the client browser.

    Just wondering if it’s possible to actually use 401 instead of 901?

    Reply
  19. anandchakru

    Thanks a lot. It really helped me save a lot of time. Wish you had given a simple example on the front-end as well.. but no issues, planning to use JQuery-ui’s modal to make the user login again. Will keep you posted on how it turns out.

    Reply
  20. FĂĄbio

    Ótimo post, parabĂ©ns.

    Reply
  21. Pingback: How to: How to manage a redirect request after a jQuery Ajax call | SevenNet

  22. Pingback: Solution: How to manage a redirect request after a jQuery Ajax call #dev #it #computers | Technical information for you

  23. Pingback: Fixed How to manage a redirect request after a jQuery Ajax call #dev #it #asnwer | Good Answer

Leave a Reply to Abdiel Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.