I\'m using grails along with spring security and angularjs. When a user session has expired and the user clicks an ajax action on the page, rather than respond with a 401, t
Did you consider "locking a screen" when the user is idle on a client-side? Of course you should handle end of a session on server-side but in fact it seems even cleaner and more secure solution than waiting for an action from client side (especially if user has left and left on a screen some sensitive data).
Check out this ng-idle directive.
I've run into a similar issue and have implemented a filter in the filter chain to detect AJAX requests and respond with a customized HTTP status (you can change it to 401 if you like).
Basically there are three parts to this. The first, is the filter. It's a servlet filter and examines the request as well as the state of the authentication in the session. Second, defining the filter as a bean within the application context in Resources.groovy
. Finally, inserting it into the Spring Security filter chain, which I've done in Bootstrap.groovy
.
I'll walk you through this now.
First the servlet filter (under src/java)
package com.xyz.security;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.util.ThrowableAnalyzer;
import org.springframework.security.web.util.ThrowableCauseExtractor;
import org.springframework.web.filter.GenericFilterBean;
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;
}
}
Second, defining the filter as a bean in the application context in Resources.groovy
beans = {
ajaxTimeoutRedirectFilter(com.xyz.security.AjaxTimeoutRedirectFilter)
}
And finally, getting the filter into the Spring Security filter chain (I used BootStrap.groovy
for this)
import grails.plugin.springsecurity.SecurityFilterPosition
import grails.plugin.springsecurity.SpringSecurityUtils
class BootStrap {
def init = { servletContext ->
SpringSecurityUtils.clientRegisterFilter('ajaxTimeoutRedirectFilter', SecurityFilterPosition.EXCEPTION_TRANSLATION_FILTER.order + 10)
}
def destroy = {
}
}