Monday, July 18, 2011

Redirect after login to requested page with Spring after CSRF protection

Bookmarks, typing URLs directly in the address bar and getting to the requested page after login are functionalities you should not break as they impact user experience.

Once you have protected your Spring website against CSRF using the Synchronizer Token Pattern you will find that the redirection to the requested page after login functionality (You request a page, the login form shows up and after that you are taken to the page you originally requested) will be broken.

Basically the user might access a bookmark or just type a URL without the security token and the redirection will use the provided (and expired) security token or no security token at all. Of course you need to hook into Spring in order to change the default functionality.

First you use "authentication-success-handler-ref" form-login property in the Spring security context:
<beans:bean id="customAuthenticationHandler" class="com.nestorurquiza.web.handler.CustomAuthenticationHandler" />
    
<form-login login-page="/login"
            authentication-success-handler-ref="customAuthenticationHandler"
            authentication-failure-url="/login?error=authorizationFailed" />

Then implement the custom handler. Code should speak for itself.
package com.nestorurquiza.web.handler;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.DefaultSavedRequest;

import com.nestorurquiza.utils.UrlTool;
import com.nestorurquiza.web.WebConstants;

public class CustomAuthenticationHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
            HttpServletResponse response, Authentication authentication)
            throws ServletException, IOException {
        // TODO Auto-generated method stub
        String ctoken = (String) request.getSession().getAttribute(WebConstants.CSRF_TOKEN);
        DefaultSavedRequest defaultSavedRequest = (DefaultSavedRequest) request.getSession().getAttribute("SPRING_SECURITY_SAVED_REQUEST_KEY");
        if( defaultSavedRequest != null && ctoken != null ) {
            String requestUrl = defaultSavedRequest.getRequestURL() + "?" + defaultSavedRequest.getQueryString();
            requestUrl = UrlTool.addParamToURL(requestUrl, WebConstants.CSRF_TOKEN, ctoken, true);
            getRedirectStrategy().sendRedirect(request, response, requestUrl);
        } else {
            super.onAuthenticationSuccess(request, response, authentication);
        }
    }
}

Here is the little useful class that allows to override the ctoken parameter (or any other url parameter)
package com.nestorurquiza.utils;

public class UrlTool {
    public static String addParamToURL(String url, String param, String value,
            boolean replace) {
        if (replace == true)
            url = removeParamFromURL(url, param);
        return url + ((url.indexOf("?") == -1) ? "?" : "&") + param + "="
                + value;
    }

    public static String removeParamFromURL(String url, String param) {
        String sep = "&";
        int startIndex = url.indexOf(sep + param + "=");
        boolean firstParam = false;
        if (startIndex == -1) {
            startIndex = url.indexOf("?" + param + "=");
            if (startIndex != -1) {
                startIndex++;
                firstParam = true;
            }
        }

        if (startIndex != -1) {
            String startUrl = url.substring(0, startIndex);
            String endUrl = "";
            int endIndex = url.indexOf(sep, startIndex + 1);
            if(firstParam && endIndex != 1) {
                //remove separator from remaining url
                endUrl = url.substring(endIndex + 1);
            }
            return startUrl + endUrl;
        }

        return url;
    }

}

16 comments:

Srinivas G said...

thanks...

nice post.I am not understanding what is the WebConstants and what it contains please tell me i am very much struc this requirement plz plz...

Nestor Urquiza said...

@Srinivas WebConstants is just a clss holding some static variables. Nothing fancy about it. Just use literals instead or your own Constants class.

Srinivas G said...

Hi Nestor...could you please provide code for WebConstants i am not getting what it is exactly...

Srinivas G said...

@Nistor my requirement is when the user select bookmarks we need to capture that bookmark url and launch the login page after login success he needs to go selected bookmark page directly..here how to capture selected bookmark url in controller i am not getting any idea about this can you please help me...

Nestor Urquiza said...

@Srinivas Here is a sample class:
public class WebConstants {
...
public static final String CSRF_TOKEN = "myToken";
...
}
Nothing fancy, just regular java way of defining constants (in this case a literal that identifies a protection token)

You don't need that part, the code here is just illustrative so you can borrow the idea as to how to accomplish your specific goal.

Your requirement about the bookmark is the same as a user refreshing a page when the session has expired. you Just need to follow the basic steps I shared here.

Look at the "First ..." and "Then ..." starting paragraphs. hyou need to hook into Spring implementation to resolve this issue. The implementation of your handler might be a little bit different.

Note that redirection to the original requested page works out of the box in Spring, just google how to do it. The issue I present in this post is how the original functionality does not work if you protect the URLs against Cross Site Request Forgery (CSRF) attacks. If you have protected your application against CSRF using a security token then this post will make sense to you.

Best,
-Nestor

Srinivas G said...

Hi Nestor..do you know how to do bookmark functionality from java code..

Nestor Urquiza said...

Hi Srinivas,

Bookmarks are usually done on the client side (browser) and most likely Java will be used in the server side (web application). However you could certainly store in the server side "favorite" urls which could be considered bookmarks (like google bookmarks)

The client side bookmarks are the one people use the most and any application will need to live with the fact that a user could bookmark a specific URL in their browser and hit it later on.

Given the user is not logged in you will need to provide a Filter that would detect the original hit URL and store it for later use (after authentication)

The mechanisms for this depend on the technology you are using for authentication. Spring is a good abstraction that will work in any application server. If you implement security through spring it will by default redirect to original request. Again this is not just special to Spring, any other authentication API you use will have the same. You can implement your own based on JEE Filter as well.

Hope that helps giving you some hints.

Best,
-Nestor

Haseeb Mirza said...

Unfortunately, This does not work for me :( Im trying with JSF, Spring

Nestor Urquiza said...

@Haseeb not sure why for JSF it would be different but definitely a question to ask to Spring community. Be prepared to share your non working test scenario.

Anonymous said...

Hi Nestor,

Thanks for nice post. I am facing one issue if # exist in my URL instead of ? as you had shown in your post. defaultSavedRequest.getQueryString() is null if URL Contains #.
Please suggest

Nestor Urquiza said...

@Praven, it should not be difficult to fix that I guess. Let us know what you did to fix it! Thanks, - Nestor

PUNIT DHIMAN said...

Getting null ctoken.

Nestor Urquiza said...

@Punit you need to implement that logic yourself. BTW newest Spring Framework versions I believe have solved the CSRF issue so you might want to take a look at the newest documentation.

Damodar said...

@Praveen and @Nestor, I would like to know whether you got a solution to read url after #. I have same issue and I could not read the url path after #.

Damodar said...

@Praveen and @Nestor, I would like to know whether you got a solution to read url after #. I have same issue and I could not read the url path after #.

Nestor Urquiza said...

@Damodar It has been a while since I last used Spring Framework. I suggest posting the question in stackoverflow. Notice that this is a very old post and newer versions might have simplified CSRF handling.

Followers