Results 1 to 8 of 8

Thread: Preserving URL anchor when redirecting

  1. #1
    Join Date
    Jul 2008
    Posts
    5

    Lightbulb Preserving URL anchor when redirecting

    I have developed a solution I'd like to share to the problem that in some cases the anchor/fragment part of a URL ("#anchor") is lost when redirects are involved in the authentication (e.g. with login pages or CAS). Anchors are commonly used in GWT applications to denote a specific (history) state in an application; a user can then create a bookmark and later come back to that state.

    See also http://forum.springsource.org/showthread.php?p=219895 or https://jira.springframework.org/browse/SEC-1067.
    As noted there, anchors are a client-side concept, so the correct handling must be performed by the browser.

    Problem:
    1. User tries to log in to <application-url>#anchor
    2. User is redirected for authentication (e.g. to CAS)
    ...
    3. User is redirected back to <application-url>

    On my test systems, Firefox keeps the #anchor when it gets a HTTP 302 redirect (i.e., it is re-appended to the redirect location). Internet Explorer (tested for IE6 and IE8) do not re-append the anchor, so it is lost in step 2. Therefore, the solution suggested in SEC-1067 does not work either.

    To make the above scenario work consistently, I have developed a filter which intercepts the redirects, and sends Javascript pages to
    * store the anchor in a cookie then redirect (to be applied to step 2)
    * restore the anchor from this cookie then redirect (to be applied to step 3)

    This solution works for me. If anyone has feedback about the appropriateness of this solution or has solved the same problem in a different way, I'd be interested. In case anyone is interested in the details, I could also post the code.

    Best regards,
    Guido

  2. #2
    Join Date
    Dec 2008
    Posts
    2

    Default I am interested in the details

    We have the same needs here. It would be great if you could post the code.

  3. #3
    Join Date
    Sep 2008
    Posts
    4

    Default Solution with Spring Security

    We had the same problem in our GWT application and I found the following solution using Spring Security:

    1. change the redirect for authentication to a forward that the original URL with the anchor isn't lost
    2. when the login form is submitted, use JavaScript to retrieve the anchor and append it to the login url with the parameter 'spring-security-redirect' of Spring Security (AbstractAuthenticationTargetUrlRequestHandler.DEF AULT_TARGET_PARAMETER)
    3. write own SavedRequestAwareAuthenticationSuccessHandler that reads the parameter and appends the anchor again before redirecting to a saved request URL


    The configuration and code looks like this:

    Code:
    <security:http auto-config="false" entry-point-ref="forwardingLoginUrlAuthenticationEntryPoint">
        ...
        <security:form-login authentication-failure-url="/login.jsp?login_error=1"
            authentication-success-handler-ref="userAuthenticationSuccessHandler" />
        ...
    </security:http>
    
    <bean id="forwardingLoginUrlAuthenticationEntryPoint"
            class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
        <property name="useForward" value="true"/>
        <property name="loginFormUrl" value="/login.jsp"/>
    </bean>
    
    <bean id="userAuthenticationSuccessHandler" class="...UserAuthenticationSuccessHandler">
        <property name="defaultTargetUrl" value="/index.jsp"/>
    </bean>
    Code:
    public class UserAuthenticationSuccessHandler extends
                  SavedRequestAwareAuthenticationSuccessHandler {
    
        /**
         * {@inheritDoc}
         */
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request,
                HttpServletResponse response, Authentication authentication)
                throws ServletException, IOException {
            SavedRequest savedRequest = requestCache.getRequest(request, response);
    
            if (savedRequest == null || isAlwaysUseDefaultTargetUrl()) {
                super.onAuthenticationSuccess(request, response, authentication);
                return;
            }
    
            clearAuthenticationAttributes(request);
    
            // Use the DefaultSavedRequest URL
            String targetUrl = savedRequest.getRedirectUrl();
    
            String gwtParameters = request.getParameter(getTargetUrlParameter());
            if (StringUtils.hasText(gwtParameters)) {
                targetUrl = targetUrl + "#" + gwtParameters;
            }
    
            getRedirectStrategy().sendRedirect(request, response, targetUrl);
        }
    }
    Code:
    <html>
    <head>
        <script type="text/javascript">
            function setSubmitUrl(form) {
                var action = "j_spring_security_check";
                var hash = self.document.location.hash;
                if (hash) {
                    var gwtAnchor = unescape(hash.substring(1));
                    action = action + "?spring-security-redirect=" + gwtAnchor;
                }
                form.action = action;
                return true;
            }
        </script>
    </head>
    <body>
        <form method="POST" onSubmit="return setSubmitUrl(this);">
        ...
        </form>
    </body>
    </html>

  4. #4
    Join Date
    Jul 2008
    Posts
    5

    Default Details for my solution (Java Code)

    The solution posted by marlov does not work in my case, as we use CAS for authentication and I cannot/do not want to include special processing into the CAS server (though theoretically it could be possible).

    Here's my solution, which does not require modifications of the login page / CAS server etc. There's certainly room for improvements, but it works in my case.

    Spring Security configuration:
    Code:
    <http...>
            ...
            <custom-filter position="FIRST" ref="retainAnchorFilter" />
            ...
    </http>
    
    <bean id="retainAnchorFilter" class="...RetainAnchorFilter">
        	<property name="storeUrlPattern" value="http://mycasserver/cas/login.*"/>
        	<property name="restoreUrlPattern" value="http://myappserver/myapp/.*"/>
        	<beans:property name="cookieName" value="TARGETANCHOR"/>
    </bean>
    storeUrlPattern controls the redirection for authentication (step 2 in my first posting), where the anchor is stored in the cookie; restoreUrlPattern controls the redirection back to the application (step 3 in my first posting).

    Here's the code for RetainAnchorFilter:
    Code:
    import java.io.IOException;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpServletResponseWrapper;
    
    import org.springframework.web.filter.GenericFilterBean;
    
    /**
     * Spring Security filter that preserves the URL anchor if the authentication process
     * contains redirects (e.g. if the login is performed via CAS or form login).
     * 
     * With standard redirects (default Spring Security behaviour),
     * Internet Explorer (6.0 and 8.0) discard the anchor
     * part of the URL such that e.g. GWT bookmarking does not work properly.
     * Firefox re-appends the anchor part.
     * 
     * This filter replaces redirects to URLs that match a certain pattern (<code>storeUrlPattern</code>)
     * with a Javascript page that stores the URL anchor in a cookie, and replaces redirects to
     * URLs that match another pattern (<code>restoreUrlPattern</code>) with a Javascript page
     * that restores the URL anchor from that cookie. The cookie name can be set via the attribute
     * <code>cookieName</code>.
     * 
     * See also http://forum.springsource.org/showthread.php?101421-Preserving-URL-anchor-when-redirecting
     */
    public class RetainAnchorFilter extends GenericFilterBean {
    
    	private String storeUrlPattern;
    	private String restoreUrlPattern;
    	private String cookieName;
    	
    	public void setStoreUrlPattern(String storeUrlPattern) {
    		this.storeUrlPattern = storeUrlPattern;
    	}
    
    	public void setRestoreUrlPattern(String restoreUrlPattern) {
    		this.restoreUrlPattern = restoreUrlPattern;
    	}
    
    	public void setCookieName(String cookieName) {
    		this.cookieName = cookieName;
    	}
    
    	@Override
    	public void doFilter(ServletRequest request, ServletResponse response,
    			FilterChain chain) throws IOException, ServletException {
    		
    		if (response instanceof HttpServletResponse) {
    			response = new RedirectResponseWrapper((HttpServletResponse)response);
    		}
    		
    		chain.doFilter(request, response);
    	}
    	
    	/**
    	 * HttpServletResponseWrapper that replaces the redirect by appropriate Javascript code.
    	 */
    	private class RedirectResponseWrapper extends HttpServletResponseWrapper {
    
    		public RedirectResponseWrapper(HttpServletResponse response) {
    			super(response);
    		}
    
    		@Override
    		public void sendRedirect(String location) throws IOException {
    			
    			HttpServletResponse response = (HttpServletResponse)getResponse();
    			String redirectPageHtml = "";
    			if (location.matches(storeUrlPattern)) {
    				redirectPageHtml = generateStoreAnchorRedirectPageHtml(location);
    			} else if (location.matches(restoreUrlPattern)) {
    				redirectPageHtml = generateRestoreAnchorRedirectPageHtml(location);
    			} else {
    				super.sendRedirect(location);
    				return;
    			}
    			response.setContentType("text/html;charset=UTF-8");
    			response.setContentLength(redirectPageHtml.length());
    			response.getWriter().write(redirectPageHtml);
    		}
    
    		private String generateStoreAnchorRedirectPageHtml(String location) {
    			
    			StringBuilder sb = new StringBuilder();
    			
    			sb.append("<html><head><title>Redirect Page</title>\n");
    			sb.append("<script type=\"text/javascript\">\n");
    			
    			// store anchor
    			sb.append("document.cookie = '" + cookieName + "=' + window.location.hash + '; path=/';\n");
    			
    			// redirect
    			sb.append("window.location = '" + location + "' + window.location.hash;\n");  
    			sb.append("</script>\n</head>\n");
    			sb.append("<body><h1>Redirect Page (Store Anchor)</h1>\n");
    			sb.append("Should redirect to " + location + "\n");
    			sb.append("</body></html>\n");
    			
    			return sb.toString();
    		}
    		
    		private String generateRestoreAnchorRedirectPageHtml(String location) {
    			
    			StringBuilder sb = new StringBuilder();
    			
    			sb.append("<html><head><title>Redirect Page</title>\n");
    			sb.append("<script type=\"text/javascript\">\n");
    			
    			// generic Javascript function to get cookie value 
    			sb.append("function getCookie(name) {\n");
    			sb.append("var cookies = document.cookie;\n");
    			sb.append("if (cookies.indexOf(name + '=') != -1) {\n");
    			sb.append("var startpos = cookies.indexOf(name)+name.length+1;\n");
    			sb.append("var endpos = cookies.indexOf(\";\",startpos)-1;\n");
    			sb.append("if (endpos == -2) endpos = cookies.length;\n");
    			sb.append("return unescape(cookies.substring(startpos,endpos));\n");
    			sb.append("} else {\n");
    			sb.append("return false;\n");
    			sb.append("}}\n");
    			
    			// get anchor from cookie
    			sb.append("var targetAnchor = getCookie('" + cookieName + "');\n");
    			
    			// append to URL and redirect
    			sb.append("if (targetAnchor) {\n");
    			sb.append("window.location = '" + location + "' + targetAnchor;\n");
    			sb.append("} else {\n");
    			sb.append("window.location = '" + location + "';\n");
    			sb.append("}\n");
    			sb.append("</script></head>\n");
    			sb.append("<body><h1>Redirect Page (Restore Anchor)</h1>\n");
    			sb.append("Should redirect to " + location + "\n");
    			sb.append("</body></html>\n");
    			
    			return sb.toString();
    		}
    		
    	}
    }
    Feedback welcome.
    Last edited by guidow08; May 18th, 2011 at 01:58 AM.

  5. #5
    Join Date
    Mar 2009
    Posts
    9

    Smile

    I've also came up with a pretty slick solution.
    I don't know whether this works smoothly with CAS authentication.
    But for form login it works quite nicely for FF and IE.

    First configure the AuthenticationEntryPoint and AuthenticationFailureHandler to use forwarding and an own login page like in the following. This also hides the custom login page name and more important, as it doesn't use a redirect but a server internal forwarding, the hash-token is preserved.

    Code:
        <bean id="authenticationEntryPoint"
              class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint"
              p:loginFormUrl="/WEB-INF/login.jsp"
              p:useForward="true"/>
    
        <bean id="authenticationFailureHandler"
              class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler"
              p:defaultFailureUrl="/WEB-INF/login.jsp?login_error=1"
              p:useForward="true"/>
    
        <security:http auto-config="true" entry-point-ref="authenticationEntryPoint">
            <security:form-login authentication-failure-handler-ref="authenticationFailureHandler"/>
            <security:intercept-url pattern="/**"
                                     requires-channel="${required.security.channel}"
                                     access="${required.role}"/>
         </security:http>
    and then create a custom login page login.jsp in your WEB-INF folder which has the following functionality:

    Code:
    <%@ page isELIgnored="false" %>
    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    
    <html>
        <head>
            <title>Login Page</title>
        </head>
        <body>
            <c:if test="${param.login_error == 1}">
                <div style="color: red">
                    Your login attempt was not successful, try again.<br/><br/>
                    Reason: ${SPRING_SECURITY_LAST_EXCEPTION.message}.
                </div>
            </c:if>
            <h3>Login with Username and Password</h3>
    
            <form name="f" action="j_spring_security_check" method="POST">
                <input type="hidden" name="spring-security-redirect"/>
                <table>
                    <tr>
                        <td><label for="username">Username:</label></td>
                        <td><input type="text" id="username" name="j_username" value="${param.j_username}"></td>
                    </tr>
                    <tr>
                        <td><label for="password">Password:</label></td>
                        <td><input type="password" id="password" name="j_password"/></td>
                    </tr>
                    <tr>
                        <td colspan="2"><input name="submit" type="submit" value="Login"/></td>
                    </tr>
                    <tr>
                        <td colspan="2"><input name="reset" type="reset" value="Reset"/></td>
                    </tr>
                </table>
            </form>
            <script type="text/javascript">
                document.f["spring-security-redirect"].value =
                <c:choose>
                    <c:when test="${not empty param['spring-security-redirect']}">
                        "${param["spring-security-redirect"]}"
                    </c:when>
                    <c:otherwise>
                        encodeURIComponent(window.location)
                    </c:otherwise>
                </c:choose>;
                document.f.j_username.focus();
            </script>
        </body>
    </html>
    Last edited by Vampire; Nov 8th, 2011 at 11:00 PM. Reason: Adjustment to login.jsp to use hidden field instead parameter

  6. #6
    Join Date
    Apr 2011
    Posts
    7

    Default

    Honestly found this solution posted on the jira issue to be the easiest to work in:


    function setSubmitUrl(form){
    var hash = unescape(self.document.location.hash.substring(1)) ;
    form.action = "resources/j_spring_security_check#" + hash;
    return true;
    }

    And then adjust your login form like...

    <form name="f" onSubmit="return setSubmitUrl(this);" method="POST">

  7. #7
    Join Date
    Nov 2012
    Posts
    8

    Default

    Great solution elementz, has anyone tested this on all browsers?

  8. #8
    Join Date
    Jan 2013
    Posts
    1

    Default Fix for RetainAnchorFilter Impl

    I am using guidow08's implementation, as well as elementz' javascript (I use this filter method to push the hash to the login page when entering the app from an external link, and the javascript to maintain the hash from the login page back to the application).

    I did find an error in the javascript in "RedirectResponseWrapper" in my implementation.

    Explanation:
    The "sendRedirect" method above is catching every login page redirect whether it has a hash on it or not. This means that the saved cookie occasionally (or mostly) has an empty value for the "cookieName" token in the browser cookie.

    The cookie is a string of tokens stored in "document.cookie," and this specific hash token may be somewhere in the middle or appended to the end of that string. If the token is in the middle of the string it is terminated with a semi-colon, otherwise it is terminated by the end of the cookie. The RedirectResponseWrapper#generateRestoreAnchorRedir ectPageHtml method has a "getCookie" function that takes this into account.

    Problem:
    The problem is that the case where the token has an empty value and occurs somewhere in the middle of the string is not handled correctly. For example, a token like "TARGETANCHOR=;".

    The "getCookie" function as-is here will return an "=" which will be appended to the end of the URL. I found this happening very consistently in chrome.

    Solution:
    Fixing the javascript solves this issue if you are seeing it. I used a stored RegExp because it proved to be the most efficient.

    Code:
    private String generateRestoreAnchorRedirectPageHtml(String location) {
    
                StringBuilder sb = new StringBuilder();
    
                // open html
                sb.append("<html><head><title>Redirect Page</title>");
                sb.append("<script type='text/javascript'>");
    
                // Create stored regex 
                // //stored regex lookup is a faster than indexOf (see http://jsperf.com/regexp-indexof-perf/30)
                // //This expression matches the token we're looking for, and the 1st group is the value.
                sb.append("var cookieParser = /" + cookieName + "=([^;]*)(?:;|$)/;");
    
                // generic Javascript function to get cookie value via regular expression
                sb.append("var getCookie = function() {");
                sb.append("  var m = cookieParser.exec(document.cookie);");
                sb.append("  if (m != null && m.length == 2) {");
                // // m[0] == full match, m[1] == the value for this token
                sb.append("    return unescape(m[1]);");
                sb.append("  } else {");
                sb.append("    return false;");
                sb.append("  }");
                sb.append("};");
    
                // get anchor from cookie
                sb.append("var targetAnchor = getCookie();");
    
                // append to URL and redirect
                sb.append("if (targetAnchor) {");
                sb.append("  window.location = '" + location + "' + targetAnchor;");
                sb.append("} else {");
                sb.append("  window.location = '" + location + "';");
                sb.append("}");
                
                // Remove cookie
                sb.append("document.cookie = '" + cookieName + "=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/';");
    
                // close html
                sb.append("</script>");
                sb.append("</head>");
                sb.append("<body>");
                sb.append("<h1>Redirect Page (Restore Anchor)</h1>");
                sb.append("<p>Should redirect to " + location + "</p>");
                sb.append("</body>");
                sb.append("</html>");
    
                return sb.toString();
            }
    Last edited by mpickell; Jan 8th, 2013 at 12:45 PM.

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •