How to persist URL hash fragments across a login redirect.

How to persist URL hash fragments across a login redirect.

I just spent four hours figuring out the ins and outs of URL hash fragments. On my team we use AngularJS to build some pretty cool single page applications. In a couple of our single page apps we use Angular routes to load various views. By default Angular uses the URL hash fragment to maintain client-side page state while still allowing the user to use the browser's forward and back buttons.

For those who don't know, hash fragments were originally implemented in browsers as a way to link to different parts of the same page. By placing an anchor tag somewhere on the page and giving it a name, you could link to that portion of the page by appending the URL with a # followed by the anchor name. When navigating to such a URL the browser would scroll to that location on the page automatically. If you were already on the page when clicking the link then the browser would know not to do another page request to the server and would instead simply jump the current page to the location of the anchor tag.

Modern JavaScript frameworks often take advantage of this browser behavior because the forward and back buttons still work, but do not cause the browser to reload the page until the user navigates to a URL that points to a different page. As long as the only thing changing in the URL is the portion after the pound sign then the browser will not do a full page request. JavaScript code can monitor the value after the hash and change things on the page as the hash fragment changes. Likewise it can update the hash fragment as the user interacts with the page. The more recent HTML 5 API brings with it a more formal mechanism for modifying the page URL without reloading the page, allowing you to avoid using the # sign. However, we're still in that time period where you can't trust that all your users have an HTML 5 capable browser.

Because of the way browsers treat hash fragments, the value of the fragment never gets sent to the server. If you're ever hoping to grab the hash fragment portion of the request URL using server-side code then you will be disappointed. This fact reared it's ugly head today when I was assigned the task of figuring out why our single page applications were losing the hash fragment state when our users' sessions expired. When a user leaves their browser open for a long time without interacting with it our server will eventually kill that user's session for security reasons. After all, we don't want lazy users forgetting to logout and allowing strangers to sit down and have complete access to their data.

When a user comes back to their computer after their session has expired and tries to perform an action on the page, the user is redirected to a login screen. The server remembers the URL they were trying to access before being taken to the login page and it appends the redirect URL as a query parameter. When the login page loads the server takes the value of that query parameter and places it inside a hidden input field within the login form.

Let's follow a user story to the login page:

  1. User gets up and leaves his/her browser open to
  2. The user comes back and refreshes their browser.
  3. Before serving up the page the server determines the user's session has expired and they are no longer authenticated.
  4. The server saves the URL the user was trying to access and then issues a 302 redirect response directing the user's browser to

Notice anything? Our hash fragment is not part of the URL encoded redirect query paremeter. That's because the hash fragment was never sent to the server with the user's page request. The browser just doesn't work that way. The server has no way of knowing what the hash fragment even was.

When the user gets to the login page the URL in their address bar looks like this: Wait. If the server never got the hash fragment then how did it end up on the end of our login page URL? Notice here that it's not part of the redirect query parameter. If it were then it would have been URL encoded like the rest of it. The browser is actually the one responsible for persisting the hash fragment to the login page. The browser will maintain hash fragments across redirects. When the browser got the signal to redirect to the login page it appended the hash fragment itself while navigating there.

Now onto the final step. The user fills out the login form, which looks something like this in the markup:

<form id="loginForm" action="login/authenticate" method="POST">
  <input name="redirect" type="hidden" value="" />
  <input name="username" type="text" />
  <input name="password" type="password" />
  <button type="submit">Login</button>

Notice that the server filled in the value of the hidden input field with our redirect URL. When the form makes its POST request to login/authenticate (notice it's not POSTing to login/authenticate#admin-console so there is no hash fragment for the browser to persist across the next redirect) then the server will verify the user's credentials and issue another redirect to the browser redirecting them back to the page they were originally trying to get to. The only problem here is that the redirect URL has lost its hash fragment because the URL the login form POSTed to didn't have a hash fragment for the browser to persist like it did when we refreshed the page we were on that already had a hash fragment. When the user arrives at the page they were trying to get to they will be surprised because the client-side code won't remember what they were doing at the time without the fragment. The user will likely get frustrated and have to redo a bunch of actions they may have already done before having to re-login to our application.

I'm embarrassed to say that it took me four hours to figure out what needed to happen here. Remember earlier that I said client-side JavaScript code can access the hash fragment easily. When the page loads there simply needs to be some JavaScript that accesses the hash fragment and appends it to the redirect URL in the hidden field. The server can issue a redirect to a URL with a fragment on the end, but only if it knows the fragment already. Here's an example using JQuery for simplicity:

$(function () {
  var $redirect = $('input[name="redirect"]');
  $redirect.val($redirect.val() + window.location.hash);

With that in place the hash fragment will make it to the server as part of the redirect URL because we've manually inserted it into the hidden form field using JavaScript. Now when the server issues its redirect response to the browser the redirect URL will contain the hash fragment and all is well!

Hopefully this bit of information saves someone from spending four hours scratching your head like I did.