I have several AngularJS apps all using Spring/Java and SAML 2.0 for SSO (leveraging the Spring Security SAML extension). My SSO id provider is OpenAM and everything is working pretty well. However, I am running into a situation when a user does a global logout from within one application but has other tabs open. Since these are single page web apps, a lot of functionality may still be usable in the orphaned tabs UNTIL, the user does something to invoke an ajax request. Of course, these AJAX requests get intercepted by the Spring Security SAML filters and triggers an authentication attempt via a REDIRECT to the OpenAM login URL. Of course, this wreaks havoc in the browser since redirects to another domain aren't allowed on AJAX requests. Furthermore, I can't really do anything with Angular's $http interceptors as the requests are 'canceled' and no quality information is available in the $http error callback function (such as a convenient 401/403 status code). All I know is that the request failed.
I don't want to assume that all bad $http requests are due to authentication problems (and do a $window.location.reload()) as there could be legitimate reasons for failure. My preference is to suppress the Spring Security redirect (to OpenAM login page) for ajax requests and, instead, send back a 401/403 status code. This would allow me to handle the error in the $http interceptor and do a full page load if it is an authentication failure, thus elegantly redirecting to the login page as if they were going to the site for the first time.
Any ideas for how to accomplish this?
The bean responsible for initialization of authentication and decision to return an HTTP error, perform a redirect, ... is an instance of AuthenticationEntryPoint
. To change its behavior you can either:
- customize the current
SAMLEntryPoint
(extend the commence
method) and override the default behavior in case request is an AJAX call from Angular, so it returns an HTTP error instead of performing redirect to IDP
- or define another
security:http
element in your Spring context (before the current one) which only covers your AJAX requests (e.g. with attribute pattern="/api/**"
) and uses an entry point which behaves in the way you want (see Http403ForbiddenEntryPoint
)
Referring to a possible implementation of Vladimir's first bullet - taken from https://www.jasha.eu/blogposts/2015/10/saml-authentication-angularjs-spring-security.html
public class XhrSamlEntryPoint extends SAMLEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
if (isXmlHttpRequest(request) && e instanceof InsufficientAuthenticationException) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
return;
}
super.commence(request, response, e);
}
private boolean isXmlHttpRequest(HttpServletRequest request) {
return "XMLHttpRequest".equalsIgnoreCase(request.getHeader("X-Requested-With"));
}
}
Keep in mind that X-Requested-With is not a mandatory header so the detection is not bullet-proof according to this answer. In my case since the backend was used with a SPA frontend, I removed the check of ajax call altogether.