25

I'm using Spring Security and jQuery in my application. Main page uses loading content dynamically into tabs via AJAX. And all is OK, however sometimes I've got the login page inside my tab and if I type credentials I will be redirected to the content page without tabs.

So I'd like to handle this situation. I know some of the people use AJAX authentication, but I'm not sure it's suitable for me because it looks quite complicated for me and my application doesn't allow any access without log into before. I would like to just write a global handler for all AJAX responses that will do window.location.reload() if we need to authenticate. I think in this case it's better to get 401 error instead of standard login form because it's easier to handle.

So,

1) Is it possible to write global error handler for all jQuery AJAX requests?

2) How can I customize behavior of Spring Security to send 401 error for AJAX requests but for regular requests to show standard login page as usual?

3) May be you have more graceful solution? Please share it.

Thanks.

informatik01
  • 16,038
  • 10
  • 74
  • 104
viator
  • 1,413
  • 3
  • 14
  • 25
  • It's been a while since you asked this. Have you come up with a good solution yourself? – Glen Little Oct 06 '10 at 17:25
  • 1
    I recently wrote a blog post on this issue: http://www.to-string.com/2012/08/03/springsecurity-authenticating-authorizing-ajax-requests/ – craftsman Aug 09 '12 at 10:36
  • I like @craftsman solution. I even simplified it (at least I think so). See http://gedrox.blogspot.com/2013/03/blog-post.html. – Gedrox Mar 08 '13 at 13:38

7 Answers7

10

Here's an approach that I think is quite simple. It's a combination of approaches that I've observed on this site. I wrote a blog post about it: http://yoyar.com/blog/2012/06/dealing-with-the-spring-security-ajax-session-timeout-problem/

The basic idea is to use an api url prefix (i.e. /api/secured) as suggested above along with an authentication entry point. It's simple and works.

Here's the authentication entry point:

package com.yoyar.yaya.config;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.*;
import java.io.IOException;

public class AjaxAwareAuthenticationEntryPoint 
             extends LoginUrlAuthenticationEntryPoint {

    public AjaxAwareAuthenticationEntryPoint(String loginUrl) {
        super(loginUrl);
    }

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

        boolean isAjax 
            = request.getRequestURI().startsWith("/api/secured");

        if (isAjax) {
            response.sendError(403, "Forbidden");
        } else {
            super.commence(request, response, authException);
        }
    }
}

And here's what goes in your spring context xml:

<bean id="authenticationEntryPoint"
  class="com.yoyar.yaya.config.AjaxAwareAuthenticationEntryPoint">
    <constructor-arg name="loginUrl" value="/login"/>
</bean>

<security:http auto-config="true"
  use-expressions="true"
  entry-point-ref="authenticationEntryPoint">
    <security:intercept-url pattern="/api/secured/**" access="hasRole('ROLE_USER')"/>
    <security:intercept-url pattern="/login" access="permitAll"/>
    <security:intercept-url pattern="/logout" access="permitAll"/>
    <security:intercept-url pattern="/denied" access="hasRole('ROLE_USER')"/>
    <security:intercept-url pattern="/" access="permitAll"/>
    <security:form-login login-page="/login"
                         authentication-failure-url="/loginfailed"
                         default-target-url="/login/success"/>
    <security:access-denied-handler error-page="/denied"/>
    <security:logout invalidate-session="true"
                     logout-success-url="/logout/success"
                     logout-url="/logout"/>
</security:http>
Matt Friedman
  • 1,535
  • 1
  • 17
  • 24
  • I implemented this approach and I catch the timeout and then redirect to the login page to login again. After login I see the ajax url and query string though. Is there a way I can get back to the page that the ajax request initiated from? Thanks – blong824 Jun 26 '12 at 19:57
  • That's a great point. That's a feature of spring security and orthogonal to this solution. However it is a problem nonetheless. I am currently experiencing the same issue and will update this post once I sort it out. Please let me know if you figure it out in the meantime. – Matt Friedman Jun 26 '12 at 21:33
  • Re: blong824's comment, this page is instructive: http://static.springsource.org/spring-security/site/docs/3.1.x/reference/appendix-namespace.html#nsa-form-login. If you set the parameter `always-use-default-target` to true you can have the system always redirect after login to the desired page. Also look for solutions related to the bean type: `SimpleUrlAuthenticationSuccessHandler`. I think more complex solutions are best described in a separate posting. – Matt Friedman Jun 27 '12 at 19:46
  • Again re: blong824's comment - Observe Raghuram's comment here: http://stackoverflow.com/questions/4696905/make-spring-security-add-the-return-to-url-in-the-query-string-for-the-login-pag?rq=1 which shows how to customize `SimpleUrlAuthenticationSuccessHandler` – Matt Friedman Jun 27 '12 at 19:55
  • I am currently trying the following: extending HttpSessionRequestCache and passing in an omit string which starts with the path for my ajax requests. I then over ride the saveRequest method and if the currentRequest does not start with the omit string I call super.saveRequest. I have a class that extends SavedRequestAwareAuthenticationSuccessHandler that checks the HttpSessionRequestCache. It is not working yet but I am getting close. If you want me to post code we should start a new question. – blong824 Jun 27 '12 at 22:01
  • This is a good solution especially when combined with the standard rest error design pattern http://blog.apigee.com/detail/restful_api_design_what_about_errors/ in my case my front end UI just prints out the user message and can display a popup ask them to login in again and continue with the original request that way they don't have to go back the original spring login page. – ams Aug 13 '12 at 11:14
9

I used the following solution.

In spring security defined invalid session url

<security:session-management invalid-session-url="/invalidate.do"/>

For that page added following controller

@Controller
public class InvalidateSession
{
    /**
     * This url gets invoked when spring security invalidates session (ie timeout).
     * Specific content indicates ui layer that session has been invalidated and page should be redirected to logout. 
     */
    @RequestMapping(value = "invalidate.do", method = RequestMethod.GET)
    @ResponseBody
    public String invalidateSession() {
        return "invalidSession";
    }
}

And for ajax used ajaxSetup to handle all ajax requests:

// Checks, if data indicates that session has been invalidated.
// If session is invalidated, page is redirected to logout
   $.ajaxSetup({
    complete: function(xhr, status) {
                if (xhr.responseText == 'invalidSession') {
                    if ($("#colorbox").count > 0) {
                        $("#colorbox").destroy();
                    }
                    window.location = "logout";
                }
            }
        });
andro83
  • 3,563
  • 2
  • 18
  • 13
  • In my case, for this to work I had to add `invalidate-session="false"` to ``, otherwise spring redirected me to `/invalidate.do` after clicking on the logout button. – matthaeus Oct 21 '13 at 12:59
4

Take a look at http://forum.springsource.org/showthread.php?t=95881, I think the proposed solution is much clearer than other answers here:

  1. Add a custom header in your jquery ajax calls (using 'beforeSend' hook). You can also use the X-Requested-With header that jQuery sends.
  2. Configure Spring Security to look for that header in the server side to return a HTTP 401 error code instead of taking the user to the login page.
Apostolos
  • 10,033
  • 5
  • 24
  • 39
Guido
  • 46,642
  • 28
  • 120
  • 174
3

I just came up with a solution to this problem, but haven't tested it thoroughly. I am also using spring, spring security, and jQuery. First, from my login's controller, I set the status code to 401:

LoginController {

public ModelAndView loginHandler(HttpServletRequest request, HttpServletResponse response) {

...
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
... 
return new ModelAndView("login", model);
}

In their onload() methods, all of my pages call a function in my global javascript file:

function initAjaxErrors() {

jQuery(window).ajaxError(function(event, xmlHttpRequest, ajaxOptions, thrownError) {
    if (403 == xmlHttpRequest.status)
        showMessage("Permission Denied");
    else
        showMessage("An error occurred: "+xmlHttpRequest.status+" "+xmlHttpRequest.statusText);
});

}

At this point, you can handle the 401 error any way you like. In one project, I have handled jQuery authentication by putting a jQuery dialog around an iframe containing a login form.

Hank
  • 31
  • 2
2

Here's how I typically do it. On every AJAX call, check the result before using it.

$.ajax({ type: 'GET',
    url: GetRootUrl() + '/services/dosomething.ashx',
    success: function (data) {
      if (HasErrors(data)) return;

      // process data returned...

    },
    error: function (xmlHttpRequest, textStatus) {
      ShowStatusFailed(xmlHttpRequest);
    }
  });

And then the HasErrors() function looks like this, and can be shared on all pages.

function HasErrors(data) {
  // check for redirect to login page
  if (data.search(/login\.aspx/i) != -1) {
    top.location.href = GetRootUrl() + '/login.aspx?lo=TimedOut';
    return true;
  }
  // check for IIS error page
  if (data.search(/Internal Server Error/) != -1) {
    ShowStatusFailed('Server Error.');
    return true;
  }
  // check for our custom error handling page
  if (data.search(/Error.aspx/) != -1) {
    ShowStatusFailed('An error occurred on the server. The Technical Support Team has been provided with the error details.');
    return true;
  }
  return false;
}
Glen Little
  • 6,951
  • 4
  • 46
  • 68
0

So there are 2 problems here. 1) Spring security is working, but the response is coming back to the browser in an ajax call. 2) Spring security keeps track of the originally requested page so that it can redirect you to it AFTER you log in (unless you specify that you always want to use a certain page after logging in). In this case, the request was an Ajax string, so you will be re-directed to that string and that is what you will see in the browser.

A simple solution is to detect the Ajax error, and if the request sent back is specific to your login page (Spring will send back the login page html, it will be the 'responseText' property of the request) detect it. Then just reload your current page, which will remove the user from the context of the Ajax call. Spring will then automatically send them to the login page. (I am using the default j_username, which is a string value that is unique to my login page).

$(document).ajaxError( function(event, request, settings, exception) {
    if(String.prototype.indexOf.call(request.responseText, "j_username") != -1) {
        window.location.reload(document.URL);
    }
});
MattC
  • 5,874
  • 1
  • 47
  • 40
0

When a timeout occurs, user is redirected to login page after any ajax action is triggered while session already cleared

security context :

<http use-expressions="true" entry-point-ref="authenticationEntryPoint">
    <logout invalidate-session="true" success-handler-ref="logoutSuccessBean" delete-cookies="JSESSIONID" />
    <custom-filter position="CONCURRENT_SESSION_FILTER" ref="concurrencyFilter" />
    <custom-filter position="FORM_LOGIN_FILTER" ref="authFilter" />
    <session-management invalid-session-url="/logout.xhtml" session-authentication-strategy-ref="sas"/>
</http>

<beans:bean id="concurrencyFilter"
  class="org.springframework.security.web.session.ConcurrentSessionFilter">
    <beans:property name="sessionRegistry" ref="sessionRegistry" />
    <beans:property name="expiredUrl" value="/logout.xhtml" />
</beans:bean>

<beans:bean id="authenticationEntryPoint"  class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
    <beans:property name="loginFormUrl" value="/login.xhtml" />
</beans:bean>

<beans:bean id="authFilter"
  class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
    <beans:property name="sessionAuthenticationStrategy" ref="sas" />
    <beans:property name="authenticationManager" ref="authenticationManager" />
    <beans:property name="authenticationSuccessHandler" ref="authenticationSuccessBean" />
    <beans:property name="authenticationFailureHandler" ref="authenticationFailureBean" />
</beans:bean>

<beans:bean id="sas" class="org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy">
    <beans:constructor-arg name="sessionRegistry" ref="sessionRegistry" />
    <beans:property name="maximumSessions" value="1" />
    <beans:property name="exceptionIfMaximumExceeded" value="1" />
</beans:bean>

Login listener :

public class LoginListener implements PhaseListener {

@Override
public PhaseId getPhaseId() {
    return PhaseId.RESTORE_VIEW;
}

@Override
public void beforePhase(PhaseEvent event) {
    // do nothing
}

@Override
public void afterPhase(PhaseEvent event) {
    FacesContext context = event.getFacesContext();
    HttpServletRequest request = (HttpServletRequest) context.getExternalContext().getRequest();
    String logoutURL = request.getContextPath() + "/logout.xhtml";
    String loginURL = request.getContextPath() + "/login.xhtml";

    if (logoutURL.equals(request.getRequestURI())) {
        try {
            context.getExternalContext().redirect(loginURL);
        } catch (IOException e) {
            throw new FacesException(e);
        }
    }
}

}