How to Share a Session Between Sinatra and Rails

As Rails developers, we run into Sinatra apps all the time: gems such as Resque which expose a dashboard via Sinatra, legacy Sinatra apps that run alongside a main Rails app, and Sinatra APIs embedded within a Rails app, to name a few examples. Here’s a common problem: how do you share authentication between the apps?

It would be really convenient to be able to do something like this in our Sinatra app:

get '/dashboard' do
  if session[:user_id].present?
    redirect to('/')
  else
    # set up and render dashboard
  end
end

Both Rails and Sinatra are Rack-based, which makes them play surprisingly well together. They can be combined in two ways: via Rack::Builder or via the Rails routes.

Rack::Builder

This method treats Rails as just another Rack app. It creates a middleware stack and mounts the apps at particular urls

# config.ru
map '/api' do
  run MySinatraApp.new
end

map '/' do
  run MyRailsApp::Application.new
end

The standard way to handle authentication in Rails is via a session that is stored in the client’s browser via a cookie. This cookie is base64 encoded in Rails 3.x and encrypted in Rails 4.x. In order to read and write from the session, Rails uses a few middlewares. They (along with the other middlewares that come by default with Rails) can be seen by running rake middleware.

  • ActionDispatch::Cookies
  • ActionDispatch::Session::CookieStore
  • Other middlewares

In order for the Sinatra app to be able to read/write from the Rails session, it needs to have those two middlewares in its stack. The middleware needs to know the name of the cookie that the session is stored in. Finally, the middleware also needs to know the secret token used for signing and encrypting the cookie

# config.ru
map '/api' do
  use Rack::Config do |env| do
    env[ActionDispatch::Cookies::TOKEN_KEY] = MyRailsApp::Application.config.secret_token
  end
  use ActionDispatch::Cookies
  use ActionDispatch::Session::CookieStore, key: '_my_rails_app_session'
  run MySinatraApp.new
end

map '/' do
  run MyRailsApp::Application.new
end

Rails routes

In the previous approach, both the Sinatra and Rails apps were first-class citizens loaded via config.ru. An easier approach is to load all the Sinatra apps via the Rails router. This automatically gives them access to the middlewares loaded (and configured) by Rails.

MyRailsApp::Application.routes.draw do
  mount MySinatraApp.new => '/api'
end

This works because the HTTP request travels through the Rails middleware stack before reaching the router which then sends it to the proper app. When using config.ru, the request is immediately routed to either the Sinatra app or the Rails app so we need to manually add the middleware in front of the Sinatra app.

Which approach to take?

Mounting a Sinatra app via the Rails routes is the standard way to embed a Sinatra app within a Rails app. Since they both share the same middleware stack, you get shared sessions for free. However, if you need a custom middleware stack for your Sinatra app then the Rack::Builder approach is the way to go.

Further reading

Learn more about Rails and Rack