Sunday, April 25, 2010

Sinatra + Warden catch

This one is gonna get a little bit technical as I've been coding a lot lately.

If you're using Warden gem with a Sinatra app authentication there's another nice gem sinatra_warden that will save you some time glueing Warden with your application.

So, your rack stack (warden + sinatra_warden) will go something like this:
Rack::Builder.app do
  use Rack::Session::Cookie
  use Warden::Manager do |manager|
    manager.default_strategies :password
    manager.failure_app = MyApp
    manager.serialize_into_session { |user| user.id }
    manager.serialize_from_session { |id| User.get(id) }
  end
  use Rack::Flash
  run MyApp
end
and everything works as expected.

Now, I often have "use Warden::Manager" inside the same app in a Sinatra extension's registered method, e.g.
def self.registered(app)
  app.use Warden::Manager do |manager|
    manager.default_strategies :password
    manager.failure_app = app
    manager.serialize_into_session { |user| user.id }
    manager.serialize_from_session { |id| User.get(id) }
  end

  app.get '/...' do
    # other stuff
  end
end
Basically, Warden's failure_app and the "main" application are the same thing.
What happens at a login failure is Warden calls the failure_app with

POST /unauthenticated

which, in turn, is being handled by sinatra_warden (0.3.0) like this:
app.post '/unauthenticated/?' do
  status 401
  env['x-rack.flash'][:error] = options.auth_error_message if defined?(Rack::Flash)
  options.auth_use_erb ? erb(options.auth_login_template) : 
                                      haml(options.auth_login_template)
end
Since we're in the same app, in my case, what happens next is Warden sees 401 response code (Unauthorized) and acts as if someone sent a wrong username/password, calling again the failure_app (which is again, "us") with POST /unauthenticated.

This goes on and on 'till (eventually) you get a "stack too deep" error.

My fix was really simple: add warden.custom_failure! to /unauthenticated block of lib/sinatra_warden/sinatra.rb (this is sinatra_warden gem)
app.post '/unauthenticated/?' do
  status 401
  warden.custom_failure! if warden.config.failure_app == self.class
  env['x-rack.flash'][:error] = options.auth_error_message if defined?(Rack::Flash)
  options.auth_use_erb ? erb(options.auth_login_template) : 
                                      haml(options.auth_login_template)
end
I really needed this quick fix so I forked sinatra_warden and published my version of the gem.

UPDATE - 26 Apr
the changes are already in the official sinatra_warden gem, version 0.3.1 so, you just need to "sudo gem update sinatra_warden" or change the version requirement to ">= 0.3.1" in your Gemfile.

1 comment:

  1. Thanks, there aren't enough good examples or explanations out there of warden/sinatra-warden, that bit about the main app and the failure app being the same was an nice piece of insight to pass on.

    ReplyDelete

What do you think?