“...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
Senior Software Engineer, 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.

GPK of the Day Mad MIKE