Session Routing via HTTP Headers with Fly and Rails 4+

TL;DR Fly Rails 4+ Session Routing is a slick and secure way to route visitor requests based on encrypted session information.

Application routing can be complicated. There's a common case that most teams will run into early on. It pops up when you have a marketing site separate from your application. Your users register and visit your site on a regular basis, but they still need to visit your marketing site to login and resume their session.

Wouldn't it be handy if we could route users based on their encrypted session information? For example: if a user visits your application at example.com without a userId stored in their session then they would land on your marketing page. If they do have a userId stored in their session then they would be routed to their application dashboard while still remaining at example.com.

Typically, this would be tricky to implement. This case is exactly what we're going to explore in this article. First, we'll get into the basics of Rails session storage. After that, we'll set-up our example application and see how we can apply the Fly Rails 4+ Session Routing Middleware to provide a smoother experience for our users.

Possibly Delicious Cookies

Since Rails 4, Rails made an adjustment to how cookies are stored.

The Cookie Monster

Coooookies?!

No! Not those cookies. These cookies:

Before Rails 4, Rails had an object known as the CookieStore. The CookieStore was placed within the client's browser and it held encoded - not encrypted - information about the user. Typically, a cookie would contain a userId and any other boolean, integer, or string information that represented a particular user - as long as it was under 4kb. This was handy! It sped up the user's experience by remembering things about them.

The fatal flaw came within the encoding. It was only done in Base64. A malicious actor could simply decode the session and hijack the information contained within the CookieStore and use it to access another user's account, among other things!

After Rails 4, the vulnerable CookieStore became the hardened EncryptedCookieStore. The sessions stored on the client became encrypted by SHA1. They could no longer be easily tampered with. Within config/secrets.yml you can see your SECRET_KEY_BASE; these keys are to be kept secret and safe, as they are used to unlock session related information.

Simple Session Application

Here's what we're going to do:

  • Create a Rails application with encrypted sessions.
  • Host our application. You can host wherever you'd like, but we'll pick Heroku.
  • Connect our application to Fly.
  • Configure the Rails 4+ Session Routing Middleware.

We want to get to the good stuff, so we'll quickly stroll through the important parts of our Rails application. Our application is a simple page with login and logout. When you login, an encrypted session is stored containing your unique userID.

Fly demonstration, showing example.com being the front for both Rails and marketing page

If you want to tinker along with this article, the source code for our application is available here.

We've created a sessions_helper.rb which dictates what we're storing in our session. We've chosen user.id and a custom remember_token that we've added to our data model.

# app/helpers/sessions_helper.rb
  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
  end

We've given the user the option to logout and in doing so clear out their session. This is important because if we're going to automatically take our users to their dashboard when they visit example.com, they need a mechanism to stop this behaviour... other than manually deleting their session cookies!

# app/helpers/sessions_helper.rb
  def log_out
    forget(current_user)
    session.delete(:user_id)
    @current_user = nil
  end

You may want to adjust what your cookie is going to be called before connecting it to Fly. We're going to call ours _fly_app_session:

# config/initializers/session_store.rb

Rails.application.config.session_store :cookie_store, key: '_fly_app_session'  

One last thing we need to ensure is that our cookies_serializer.rb is set to :json format. Rails previously defaulted to :marshal. JSON is easier to handle than its predecessors.

# config/initializers/cookies_serializer.rb

Rails.application.config.action_dispatch.cookies_serializer = :json  

Next, we'll need to get our application hosted.

goodroot@superfly ~/Fly/fly_rails_session$ git push heroku master  
...
remote:        https://fly-rails-session.herokuapp.com/ deployed to Heroku  

Excellent! We'll need to make note of our Heroku application name: fly-rails-session. Now, let's Fly.

Fly!

Fly is designed to help you deliver applications. As we revealed in the introduction, we are going to use it to intelligently route example.com between our marketing page and our Rails application. Let's explore that, briefly.

Static sites make excellent marketing pages. GitHub Pages, for example, can provide a simple and free way of creating and hosting a static site. Let's consider that we have set-up our static marketing page to be on the root of our hostname, example.com. Somewhere within your marketing page, you'll have signup and login links. We'll want to point them to example.com/app/signup and example.com/app/login, respectively.

If you need a hand configuring a static page for this example, no problem: we have a guide for you.

We want our application to live on the "root" hostname, /, but we'll need to mount it on its own subfolder, such as example.com/app. To provide a more seamless and intuitive experience for our visitor, we'd prefer if those who are logged in receive their dashboard when they visit example.com instead of the marketing page.

Let's add our Rails application and connect the Fly Agent to Heroku. Within Fly, visit your site, click Routing, then Add Backend within the Backends tab. We'll choose Heroku, fill in our repository name, then specify /app/ as our mount path.

Our next step is to add the Fly Buildpack and the FLY_TOKEN.

We can do this via the command line...

heroku buildpacks:add https://github.com/superfly/fly-heroku-buildpack  
heroku config:set FLY_TOKEN=5fab46327d83364af3125c039738740c1e021de55439d4317444fe7b7be5fea5  

Your Rails application is now connected to Fly - fantastic! If we try to visit example.com/app now, we'll notice that... it's broken. No worries. We need to make a few alterations to our Rails application. We're configuring our demo to be on example.com/app, but if you'd like to use something else be sure to change the routes accordingly.

First, we're going to want to scope our application routes with the /app prefix:

# config/routes.rb
  scope '/app' do
    get 'sessions/new'
    get '/signup',    to: 'users#new'
    post '/signup',   to: 'users#create'
    get '/login',     to: 'sessions#new'
    post '/login',    to: 'sessions#create'
    delete 'logout',  to: 'sessions#destroy'
  end

Next, let's specify a config.assets.prefix to ensure all of our static assets will appear on our custom /app/ path:

# config/application.rb
  class Application < Rails::Application
    config.assets.prefix = "/app/assets/"
  end

Once we push our changes up and Heroku builds, we can visit example.com/app/login to see our application's login page as we expect it. Now, with both our static marketing page and our application mounted we can get into the Fly routing magic.

Magical Movement

Head to the Middleware tab within your site and enable the Rails 4+ Sesssion Routing Middleware. You'll be prompted to put in two pieces of information: the Session Cookie Name and the Session Cookie Encryption Key. Given that your client-side Rails sessions are encrypted, we'll need to know the name of the session and the safely kept value of the SECRET_KEY_BASE.

Fly stores this information securely. Providing Fly with the SECRET_KEY_BASE gives our edge-servers the ability to peek into the headers and route requests based on their contents. We added the Fly Agent earlier; the Fly Agent creates an encrypted tunnel between our edge-server and your application. Your data is secured over HTTPS to the edge-server and then through encrypted tunnels directly to your application: end-to-end encryption.

Once the Middleware has been enabled, we can use the HTTP Header field within Routing Rules. Each key stored in the session cookie can be represented via an HTTP Header. For example, userId: becomes Fly-User-Id. Let's see this in action. In Fly, head to your site, visit Routing, then click Add routing rule.

To make us more comfortable with what we're setting, we'll do a breakdown of the form fields:

Priority: When the route will apply itself. For this route, we want the priority to be higher (less than) the priority of our usual / route.

Request Scheme: We can specify HTTP or HTTPS, or all. We'd like all values.

Hostname: If we have multiple hostnames, we can specify them or match on all of them. Useful if you're applying a subdomain or a separate TLD.

HTTP Header: With Middleware enabled, all our JSON-ified session cookie fields can be turned into headers; userId becomes Fly-User-Id.

HTTP Header Content to Match: We can use regex to match for any permutation that appears as a header value. For our demo, we're using \d+ which is any digit greater than one. It's a simple way of asking: do you have a sessionID?

Path to Match: Requests to this path will search for a matching HTTP Header. We want it to be our root domain, example.com, so the value should be /.

Route to Backend: Our Heroku application will appear here. Select it!

Path Rewrite: We do not need this field in our example. You'd use it to manipulate the path that the server is expecting.

Cool. To conclude, we'll weave everything together.

When we visit example.com, we'll see our marketing site. When you code your login links and signup links, you'd set them to: example.com/app/login and example.com/app/signup respectively. After login or signup, our application is redirected back to example.com. Our primary hostname now links directly to our application. On logout or when a new user visits example.com, it'll be our marketing page again.

Our marketing site gracefully vanishes and we see that we're now within our application. The presence of userId in the session cookie matches with our route pattern; this is quite neat! If you were to delete the _fly_app_session cookie or logout, we'd return back to our marketing page.

Summary

We voyaged through one excellent use-case for the Fly Rails 4+ Session Middleware: serving your application from / when a userId is detected and your marketing page when one is not. With the ability to create HTTP headers based on your secured session cookie values, you can route on gnarly patterns of all sorts. Did the user save a device preference? Send them mobile. Is a language set? Send them regionalized content.

If hanging out and building things like this with developers of all skill levels is your thing, we're trying to get a Slack community going - stop on by!

Evan Larsson

A polite, forest-dwelling Canadian who enjoys coding and writing. He's spent almost two decades building web applications and strives to keep development fun and light-hearted.

Superfly Your Applications

Fly is an Application Delivery Network. In a few minutes, you'll have access to tools that save your engineers time and make delivering your application a breeze.

Free Signup