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
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.
No! Not those cookies. These cookies:
Before Rails 4, Rails had an object known as 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
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
# 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 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
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
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
# 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
# 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.
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
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,
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
Request Scheme: We can specify
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;
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/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.
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!