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