“...I've been working since 2008 with Ruby / Ruby on Rails, love a bit of Elixir / Phoenix and learning Rust. I also poke through other people's code and make PRs for OpenSource Ruby projects that sometimes make it. Currently working for InPay...”

Rob Lacey (contact@robl.me)
Senior Software Engineer, Brighton, UK

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.