Distigme

A little learning is a dangerous thing.

Ajax and Spring Security form-based login

5 Comments

If your web application uses a form-based login through Spring security, and the same application uses ajax, you likely have a problem. Say your user opens two tabs for your application in the browser and logs out from one of them, while the other is periodically updating via ajax. Or, say your user comes back after lunch, to a page whose session has long timed out, and does something that attempts ajax. Or, say the user just pushes the Back button after logging out or timing out, and that pages uses some ajax. The default behavior is to treat the ajax request like any other web page and redirect to the login form. Not good.

Aside from the obvious absurdity, there are some real problems. First, it is not straightforward for your JavaScript code to determine what went wrong; it just sees a redirect followed by a successful GET of the login page, which looks fine and dandy apart from being HTML instead of JSON. Second, if the user, in another tab, tries to access a secured page, and an ajax call hits while they are typing in their password, the user will end up looking at a page full of JSON instead of their intended destination. That’s because Spring Security, by default, determines the destination after login from whatever was most recently stored in the session. (Really? The session??? I guess that’s because if you submit a form on an expired session, you don’t want to lose all your typing, but there’s no better place to keep the form data.)

Umar offers a solution based upon writing a custom ExceptionTranslationFilter, which ends up being really cumbersome because the <http> security namespace does not play nicely with it and has to be expanded into all its gory innards.

My solution is similar but overcomes this difficulty. The two steps are to configure the request cache to reject ajax requests and to replace the authentication entry point with one that rejects ajax requests.

<bean id="nonAjaxRequestMatcher" class="org.example.NonAjaxRequestMatcher" />

<bean id="loginUrlAuthenticationEntryPoint" 
	class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
	<constructor-arg value="/login" />
</bean>

<bean id="ajaxAuthenticationEntryPoint" 
	class="org.springframework.security.web.authentication.Http403ForbiddenEntryPoint" />

<bean id="authenticationRequestCache" 
	class="org.springframework.security.web.savedrequest.HttpSessionRequestCache">
	<property name="requestMatcher" ref="nonAjaxRequestMatcher" />
</bean>

<bean id="authenticationEntryPoint" 
	class="org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint">
	<constructor-arg>
		<map>
			<entry key-ref="nonAjaxRequestMatcher" value-ref="loginUrlAuthenticationEntryPoint" />
		</map>
	</constructor-arg>
	<property name="defaultEntryPoint" ref="ajaxAuthenticationEntryPoint" />
</bean>

<security:http entry-point-ref="authenticationEntryPoint" ...>
	<security:request-cache ref="authenticationRequestCache" />
	...
</security:http>

This works by defining a criterion nonAjaxRequestMatcher that will identify only non-ajax pages that should be redirected as needed to the login page. This can be plugged directly into the authentication request cache. The authentication entry point is a bit more complicated, but it can all be set up in Spring as shown. A DelegatingAuthenticationEntryPoint is used to ask which type of request we’re dealing with and then send interactive requests to a standard loginUrlAuthenticationEntryPoint and ajax requests to ajaxAuthenticationEntryPoint, which returns 403 Forbidden.

The NonAjaxRequestMatcher can be implemented simply:

public class NonAjaxRequestMatcher implements RequestMatcher {
	@Override
	public boolean matches(HttpServletRequest request) {
		return !"XmlHttpRequest".equalsIgnoreCase(request.getHeader("X-Requested-With"));
	}
}

This bit of magic relies on a convention built into most modern JavaScript frameworks (Dojo, JQuery, etc.) to identify ajax requests by a special header.

Lastly, you can also go one step further and write a custom replacement for Http403ForbiddenEntryPoint that will return JSON instead of HTML, as many JavaScript frameworks seem surprised when you say to expect JSON and the error page has HTML instead (!).

5 thoughts on “Ajax and Spring Security form-based login

  1. This is a great solution! Thanks a lot for sharing this.

  2. Nice and Neat solution

  3. Pingback: ajax - Printemps de sécurité + Ajax d'expiration de la session problème

Leave a reply to JH Cancel reply