stderr

secondary output stream of a software developer

Making Rails 4 and Sinatra share a Session

In our latest project, I wanted to restrict access to the resque web interface by reusing the Warden/Devise/CanCan based authorization stack of the main Rails app. I quickly found some posts explaining how to mount the resque Sinatra app side by side with the Rails app in the config.ru file. Still, I always ended up with an empty env['rack.session'] inside the Sinatra app.

As it turns out, Rails 4 changes the session middleware to use encrypted cookies. It no longer relies on Rack::Session::Cookie, but uses ActionDispatch::Session::CookieStore instead.

Unfortunately, the new CookieStore middleware cannot live on its own since the ActionDispatch::Cookies::CookieJar it uses to get cookies requires an action_dispatch.key_generator to be present on the env. If it is missing, the session cookie cannot be decrypted leading to silent failure.

Rails::Application objects place the key generator on an env_config hash, which is merged into the request's env before processing a request. To make the Rails session available to our Sinatra app, we have to insert a middleware which mimics this behaviour:

# rails_env_config_middleware.rb 
class RailsEnvConfigMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    env.merge!(Rails.application.env_config)
    @app.call(env)
  end
end

The final rack up file then looks like this:

# config.ru
require ::File.expand_path('../config/environment',  __FILE__)

map '/' do
  run Rails.application
end

map '/resque' do
  # This is the important line
  use RailsEnvConfigMiddleware

  use ActionDispatch::Session::CookieStore, :key => '_<app_name>_session', 
    :path => '/', :secret => '<secret_key_base>'

  use Warden::Manager do |manager|
    manager.failure_app = Pageflow::Application
    manager.default_scope = Devise.default_scope
  end

  run AdminResqueServer.new
end

Now we can access the authenticated user via warden and authorize with CanCan:

# admin_resque_server.rb
require 'resque/server'
require 'resque_scheduler/server'

class AdminResqueServer < Resque::Server
  before do
    redirect '/admin/login' unless authenticated?
    redirect '/' unless can?(:manage, Resque)
  end

  private

  def can?(*args)
    Ability.new(user).can?(*args)
  end

  def authenticated?
    request.env['warden'].authenticated?
  end

  def user
    request.env['warden'].user
  end
end

In our production environment, I faced a final bug. For some reason the rack-protection middleware which comes as part of the Sinatra stack kept dropping the session as a reaction to some falsely detected attack. Deactivating protection inside the Sinatra app solved the issue for now.

class AdminResqueServer < Resque::Server
  disable :protection

  ...
end

This is of course not the desired solution. I'll post an update once I have figured out the details.

comments powered by Disqus