Devise and nuking sessions - Magical Devise Journey II
Not a few days ago I was patting myself on the back for my Magical Devise Journey forcing only particular Devise strategies in some controllers. So after all that. I realised that is no good if we are logged in and that authentication is stored I am still logged in if I try to access API endpoints from the browser. What gives?
Well I was all obsessed over the strategies passed in as args I didn’t spot something.
def _perform_authentication(*args)
scope, opts = _retrieve_scope_and_opts(args)
user = nil
# Look for an existing user in the session for this scope.
# If there was no user in the session, see if we can get one from the request.
return user, opts if user = user(opts.merge(:scope => scope))
_run_strategies_for(scope, args)
if winning_strategy && winning_strategy.successful?
opts[:store] = opts.fetch(:store, winning_strategy.store?)
set_user(winning_strategy.user, opts.merge!(:event => :authentication))
end
[@users[scope], opts]
end
Hang on, if you find the user for that scope return them or try our auth strategies..
return user, opts if user = user(opts.merge(:scope => scope))
_run_strategies_for(scope, args)
Dang!
def user(argument = {})
opts = argument.is_a?(Hash) ? argument : { :scope => argument }
scope = (opts[:scope] ||= @config.default_scope)
if @users.has_key?(scope)
@users[scope]
else
unless user = session_serializer.fetch(scope)
run_callbacks = opts.fetch(:run_callbacks, true)
manager._run_callbacks(:after_failed_fetch, user, self, :scope => scope) if run_callbacks
end
@users[scope] = user ? set_user(user, opts.merge(:event => :fetch)) : nil
end
end
So where am I finding it if it’s not by strategy….oh right. I am logged in via a session cookie.
def fetch(scope)
key = session[key_for(scope)]
return nil unless key
method_name = "#{scope}_deserialize"
user = respond_to?(method_name) ? send(method_name, key) : deserialize(key)
delete(scope) unless user
user
end
So I need to nuke that session in my API controllers.
I seem to remember the preferred way to nuke sessions in controllers was something like
class ApiController < ApplicationController
protect_from_forgery with: :null_session
end
That’s the skinny. It nullifies the session and cookies in some magic Rails-y way.
module ActionController #:nodoc:
module RequestForgeryProtection
module ProtectionMethods
class NullSession
def initialize(controller)
@controller = controller
end
# This is the method that defines the application behavior when a request is found to be unverified.
def handle_unverified_request
request = @controller.request
request.session = NullSessionHash.new(request.env)
request.env['action_dispatch.request.flash_hash'] = nil
request.env['rack.session.options'] = { skip: true }
request.env['action_dispatch.cookies'] = NullCookieJar.build(request)
end
end
end
end
end
But…..
module ActionController #:nodoc:
module RequestForgeryProtection
def verify_authenticity_token
mark_for_same_origin_verification!
if !verified_request?
if logger && log_warning_on_csrf_failure
logger.warn "Can't verify CSRF token authenticity"
end
handle_unverified_request
end
end
def verified_request?
!protect_against_forgery? || request.get? || request.head? ||
valid_authenticity_token?(session, form_authenticity_param) ||
valid_authenticity_token?(session, request.headers['X-CSRF-Token'])
end
end
end
Dang! Dang! Looks like it doesn’t even bother to run the forgery strategy if it’s a GET. So it will nuke sessions but not unless the request should be verified. No worries we’ll just re-use the method of nuking the session and cookies.
class ApiResourceController < ApplicationController
prepend_before_filter do
ActionController::RequestForgeryProtection::ProtectionMethod::NullSession.new(self).handle_unverified_request
end
end
Magic.