I have seen some blogs about Spring Security 3 Ajax login, however I could not find any that tackles how to invoke Ajax based login, where a protected resource is being accessed in Ajax by an anonymous user.
The problem - The web application enables anonymous access to certain parts and certain parts are protected resources which require the user to login.
When an anonymous user accesses protected resources (via Http Get / Post), Spring Security automatically invokes the login page and after a successful authentication, redirects to the required resource/page.
However, if the protected resource is being accessed in Ajax, the login page will not appear correctly (will be set on part of the page). The 302 code (redirect to login page) will not function as expected in Ajax.
Please note that this is NOT the same as initiating an Ajax login screen (e.g. when user press on the login button and a popup with user/password fields is being invoked).
So - how can we have Spring Security 3 handle access to protected resources both with "regular" HTTP Post(FORM based authentication) AND Ajax calls, including a redirect to the required resource after successful authentication?
So, this blog post has two protection layers/parts:
1. Spring Security 3 standard FORM based authentication
2. Configure/extends Spring Security 3 and the app to support also Ajax access to protected resources.
Regarding part 1 - there are many references about the issue. No need to elaborate.
Regarding part 2 - Requires the following:
1. Configure Spring Security 3 to enable Ajax based login.
2. Configure client Ajax calls to protected resources to handle request for authentication.
3. Re-execution of functions to simulate the automatic user original method invocation after successful login (as it happens in the FORM based login)
The below diagram describes a detailed flow and should help follow the client/sever communication.
|
Handling protected resource access via Ajax |
|
|
|
|
|
|
Lets discuss the diagram:
The flow starts with an anonymous user Ajax request to a protected resource (1). In this case the user wants to add an item to the shopping cart.
The addItem method is a protected resource, which is protected via Spring Security (@pre_authorize("SOME_ROLE")) (2). This causes the Spring Secutiry filter (3) to send the login FORM with HTTP code 302 (i.e. redirect to that page).
Now, since this is an Ajax call, it will not handle the request well, so here comes the part that takes the login FORM, put it aside, and invoke Ajax based login instead (4):
The client Ajax method (which invoked the Ajax addItem method) checks whether it is a form based login or any other reply. If it is a FORM based login, it will call a dialog modal (5) that will try to login in Ajax. Spring will handle the Ajax login authentication (6) and return an appropriate message to the client. The client, if the message was successful, will re-execute the original function, which tried to access the protected resource (e.g.
addItem in our example).
Let us see how it all fits in our code:
Steps #1, #4 - Client side which accesses protected resources and checks if a login is required
//JavaScript method - Ajax call to protected resource (#1 in flow diagram)
function addItem(itemId) {
$.ajax({
url: '/my_url/order/addItem',
type: 'POST',
data: ({orderItemId : itemId,...}),
success: function(data) {
//construct a callback string if user is not logged in.
var cllbck = 'addItem('+itemId +')';
//Client check if login required
//(#4 in flow diagram)
if (verifyAuthentication(data,cllbck)){
// in here => access to protected resource was ok
// show message to user, "item has been added..."
}
});
}
Steps #2, #3 - is a regular Spring Security configuration. Plenty of
resources out there.
Step #4 - Client checks if login is required:
function verifyAuthentication(data, cllBackString){
//naive check - I put a string in the login form, so I check for existance
if (isNaN(data) && (data.indexOf("login_hidden_for_ajax")!= -1)){
//if got here then data is a loginform => login required
//set callback in ajax login form hidden input
$("#my_callback").val(cllBackString);
//show ajax login
//Get the window height and width
var winH = $(window).height();
var winW = $(window).width();
//Set the popup window to center
$("#ajaxLogin").css('top', winH/2-$("#ajaxLogin").height()/2);
$("#ajaxLogin").css('left', winW/2-$("#ajaxLogin").width()/2);
$("#ajaxLogin").fadeIn(2000);
return false;
}
// data is not a login form => return true to continue with function processing
return true;
}
Step #5, #7 - the Ajax login FORM utilizes the following Ajax login:
function ajaxLogin(form, suffix){
var my_callback = form.my_callback.value; // The original function which accessed the protected resource
var user_pass = form.j_ajax_password.value;
var user_name = form.j_ajax_username.value;
//Ajax login - we send credentials to j_spring_security_check (as in form based login
$.ajax({
url: "/myContextURL/j_spring_security_check",
data: { j_username: user_name , j_password: user_pass },
type: "POST",
beforeSend: function (xhr) {
xhr.setRequestHeader("X-Ajax-call", "true");
},
success: function(result) {
//if login is success, hide the login modal and
//re-execute the function which called the protected resource
//(#7 in the diagram flow)
if (result == "ok") {
$("#ajax_login_error_"+ suffix).html("");
$('#ajaxLogin').hide();
if (my_callback!=null && my_callback!='undefined' && my_callback!=''){
eval(my_callback.replace(/_/g,'"'));
}
return true;
}else {
$("#ajax_login_error_"+ suffix).html('<span class="alert display_b clear_b centeralign">Bad user/password</span>') ;
return false;
}
},
error: function(XMLHttpRequest, textStatus, errorThrown){
$("#ajax_login_error_"+ suffix).html("Bad user/password") ;
return false;
}
});
}
We need to set Spring to support Ajax login (#6):
Set Spring Security xml configuration:
...
Define a handler for login success:
@Component("ajaxAuthenticationSuccessHandler")
public class AjaxAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
public AjaxAuthenticationSuccessHandler() {
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
HttpSession session = request.getSession();
DefaultSavedRequest defaultSavedRequest = (DefaultSavedRequest) session.getAttribute(WebAttributes.SAVED_REQUEST);
//check if login is originated from ajax call
if ("true".equals(request.getHeader("X-Ajax-call"))) {
try {
response.getWriter().print("ok");//return "ok" string
response.getWriter().flush();
} catch (IOException e) {
//handle exception...
}
} else {
setAlwaysUseDefaultTargetUrl(false);
...
}
}
}
Define a handler for login failure - the same as success, but the string is "not-ok".
I know some of the code here is not the best practice so I would like to hear what you think.
Please post me if you can see a way to improve the process or make it more generic.
Acknowledgment -
Diagram was done via
gliffy - online diagram tool