Announcement Announcement Module
Collapse
No announcement yet.
Preserving URL anchor when redirecting Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • 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
    I am interested in the details

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

    Comment


    • #3
      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>

      Comment


      • #4
        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, 02:58 AM.

        Comment


        • #5
          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 9th, 2011, 12:00 AM. Reason: Adjustment to login.jsp to use hidden field instead parameter

          Comment


          • #6
            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">

            Comment


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

              Comment


              • #8
                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, 01:45 PM.

                Comment

                Working...
                X