28

I have RESTful API written on RoR 3. I have to make my application not to send "Set-Cookie header" (clients are authorizing using auth_token parameter).

I have tried to use session :off and reset_session but it does not make any sense. I am using devise as authentication framework.

Here is my ApplicationController

class ApplicationController < ActionController::Base
  before_filter :reset_session #, :unless => :session_required?
  session :off #, :unless => :session_required?

  skip_before_filter :verify_authenticity_token
  before_filter :access_control_headers!

  def options
    render :text => ""
  end

  private
  def access_control_headers!
    response.headers["Access-Control-Allow-Origin"] = "*"
    response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"
    response.headers["Access-Control-Allow-Credentials"] = "true"
    response.headers["Access-Control-Allow-Headers"] = "Content-type"
  end

  def session_required?
    !(params[:format] == 'xml' or params[:format] == 'json')
  end
end
Matteo Alessani
  • 10,264
  • 4
  • 40
  • 57
Andrey Kuznetsov
  • 11,640
  • 9
  • 47
  • 70
  • I similarly wanted to prevent session creation, but store my sessions in the DB so suppressing cookies doesn't cut it. My solution was to tell Rack to drop the session: http://stackoverflow.com/questions/33318060/how-do-i-prevent-rails-from-creating-a-session/ – Drew Stephens Oct 24 '15 at 12:22

10 Answers10

46

Use the built in option.

env['rack.session.options'][:skip] = true

or the equivalent

request.session_options[:skip] = true

You can find the documentation for it here https://github.com/rack/rack/blob/master/lib/rack/session/abstract/id.rb#L213

Paweł Gościcki
  • 9,066
  • 5
  • 70
  • 81
Darwin
  • 4,686
  • 2
  • 30
  • 22
34

As is mentioned in a comment on John's answer, clearing the session will not prevent the session cookie from being sent. If you wish to totally remove the cookie from being sent, you have to use Rack middleware.

class CookieFilter
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, body = @app.call(env)

    # use only one of the next two lines

    # this will remove ALL cookies from the response
    headers.delete 'Set-Cookie'
    # this will remove just your session cookie
    Rack::Utils.delete_cookie_header!(headers, '_app-name_session')

    [status, headers, body]
  end
end

Use it by creating an initializer with the following body:

Rails.application.config.middleware.insert_before ::ActionDispatch::Cookies, ::CookieFilter

To prevent the cookie filter to end up in application stack traces, which can be utterly confusing at times, you may want to silence it in the backtrace (Assuming you put it in lib/cookie_filter.rb):

Rails.backtrace_cleaner.add_silencer { |line| line.start_with? "lib/cookie_filter.rb" }
troelskn
  • 115,121
  • 27
  • 131
  • 155
Ryan Ahearn
  • 7,886
  • 7
  • 51
  • 56
  • It removes `flash[]`. With this middleware class it is impossible to set some value in the `flash[]` in one action, make `redirect_to` to another action and get your value from `flash[]` in this another action. It also doesn't allow to set custom cookies or session if you want to persist data in them between redirections. This class just removes everything. – Green Jun 05 '13 at 02:01
  • 2
    I realise this is an old comment, but what's wrong with changing initializers/session_store.rb to contain: `PlayreadyLicense::Application.config.session_store :disabled` in order to disable session cookies? – julianc Dec 12 '13 at 23:16
  • Is this valid for Rails4? – Saad Masood Oct 16 '14 at 06:37
  • 1
    @SaadMasood Use `request.session_options[:skip] = true` instead. See Darwins answer – karlingen Dec 23 '15 at 08:21
8

I'm not sure when they added it to Devise, but there appears to be a configuration that will let you disable the sending of the session cookie when using a auth_token:

# By default Devise will store the user in session. You can skip storage for
# :http_auth and :token_auth by adding those symbols to the array below.
# Notice that if you are skipping storage for all authentication paths, you
# may want to disable generating routes to Devise's sessions controller by
# passing :skip => :sessions to `devise_for` in your config/routes.rb
config.skip_session_storage = [:http_auth, :token_auth]

It does work well. The only issue I had was that I still needed to be able to make an initial request to my token_controller in order to generate/retrieve the token. I.e. POST /api/v1/tokens.json, which unfortunately would cause a session cookie to be returned for that request.

So I ended up implementing the CookieFilter intializer that Ryan Ahearn wrote above anyway.

Also, since my app has both a web front-end as well as a JSON api, I only wanted to filter the cookies for the JSON api. So I modified the CookieFilter class to first check the requests belonged to the api:

if env['PATH_INFO'].match(/^\/api/)
  Rack::Utils.delete_cookie_header!(headers, '_myapp_session')
end

Not sure if there's a better way of doing that...

asgeo1
  • 9,028
  • 6
  • 63
  • 85
  • How are you able to use the devise helpers like `signed_in` or `current_user` without the session? – Nathan Apr 11 '15 at 04:22
5

Another solution: In the controller you want to avoid cookies, add this:

after_filter :skip_set_cookies_header

def skip_set_cookies_header
  request.session_options = {}
end

If you have a set of api controllers, set this in an api_controller class and let your other controllers inherit the api_controller.

This skips setting Set-Cookie header since the session opts is empty.

tommy chheng
  • 9,108
  • 9
  • 55
  • 72
  • This worked great on my Rails 4 app, but caused (hard to find) runtime errors after upgrading to Rails 5. Just a heads up for anyone who starts seeing "Undefined method id for {}" errors in their stack trace. – Rick Nov 29 '16 at 14:45
4

The default CookieSessionStore doesn't send a "Set-Cookie" header unless something is added to the session. Is something in your stack writing to the session? (it's probably Devise)

session :off has been deprecated:

def session(*args)
  ActiveSupport::Deprecation.warn(
    "Disabling sessions for a single controller has been deprecated. " +
    "Sessions are now lazy loaded. So if you don't access them, " +
    "consider them off. You can still modify the session cookie " +
    "options with request.session_options.", caller)
end

If something in your stack is setting session info, you can clear it using session.clear like so:

after_filter :clear_session

def clear_session
  session.clear
end

Which will prevent the Set-Cookie header from being sent

John Douthat
  • 40,711
  • 10
  • 69
  • 66
  • 2
    session.clear removes the entire user's session. It does not prevent the Set-Cookie header from being sent; instead, it issues a new Set-Cookie that completely empties the user's session. – stephenjudkins Sep 15 '11 at 17:56
  • 1
    @john "CookieSessionStore doesn't send a "Set-Cookie" header unless something is added to the session" I thought so too! but Set-Cookie is now set at Rack level action_dispatch/middleware/cookies.rb which is duh – choonkeat Nov 16 '11 at 07:11
3

Further to John's answer, if you are using CSRF protection you would need to turn that off for web service requests. You can add the following as a protected method in your application controller:

  def protect_against_forgery?
    unless request.format.xml? or request.format.json?
      super
    end
  end

This way HTML requests still use CSRF (or not - depends on config.action_controller.allow_forgery_protection = true/false in the environment).

Sayantam
  • 914
  • 8
  • 5
  • I have found that simply removing `<%= csrf_meta_tags %>` from my layout did the trick. – Jo Liss Feb 09 '12 at 01:33
  • 1
    @JoLiss That would mean you're compromising CSRF protection -- not a reasonable solution to the problem. The meta tags are used for verifying AJAX requests that modify data (non-`GET`), so you'd likely get exceptions on those types of requests by leaving them out. What Sayantam is trying to illustrate shouldn't be necessary though, as Rails only verifies HTML and JS (not JSON) requests anyway -- see: http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection.html and http://weblog.rubyonrails.org/2011/2/8/csrf-protection-bypass-in-ruby-on-rails. – ches Feb 18 '12 at 07:39
  • Right, thanks for pointing this out. I was doing this for a site that doesn't need CSRF protection -- in general, removing the csrf_meta_tags is in fact not a good idea. – Jo Liss Feb 18 '12 at 17:54
2

I myself truly missed being able to declaratively turn off sessions (using session :off)

... thus I brought it "back" - use it just like in plain-old-rails (<= 2.2) :

than of course this might require some additional Devise specific hacking of your own, since session_off might cause session == nil in a controller, and most rails extensions since 2.3 simply assume a lazy session that shall not be nil ever.

https://github.com/kares/session_off

kares
  • 7,076
  • 1
  • 28
  • 38
1

Imo the best approach is to simply remove the cookie session store middleware.

To do so, add this to your application.rb (or to a specific environment if needed):

# No session store
config.middleware.delete ActionDispatch::Session::CookieStore
gucki
  • 4,582
  • 7
  • 44
  • 56
0
# frozen_string_literal: true

module Api
  module Web
    module Base
      class WebApiApplicationController < ApplicationController

        include DeviseTokenAuth::Concerns::SetUserByToken
        include Api::Concerns::ErrorsConcern

        devise_token_auth_group :user, contains: %i[api_web_v1_user]
        respond_to :json
        serialization_scope :current_user

        before_action :METHOD_NAME

        private

        def METHOD_NAME
          request.session_options[:skip] = true
        end

      end
    end
  end
end

It's working for me.

0

Try this instead

after_filter :skip_set_cookies_header

def skip_set_cookies_header
  session.instance_variable_set('@loaded', false)
end

Or even better, always remove Set-Cookie header when session data did not change

before_filter :session_as_comparable_array # first before_filter
after_filter :skip_set_cookies_header      # last  after_filter

def session_as_comparable_array(obj = session)
  @session_as_comparable_array = case obj
  when Hash
    obj.keys.sort_by(&:to_s).collect{ |k| [k, session_as_comparable_array(obj[k])] }
  when Array
    obj.sort_by(&:to_s).collect{ |k| session_as_comparable_array(k) }
  else
    obj
  end
end

def skip_set_cookies_header
  session.instance_variable_set('@loaded', false) if (@session_as_comparable_array == session_as_comparable_array)
end
choonkeat
  • 5,557
  • 2
  • 26
  • 19
  • 1
    I think it goes without saying that digging into an object's instance variables like this is a really dirty hack, and you're asking for it to break when the underlying implementation details are changed in a future update :-P – ches Feb 18 '12 at 07:43