12

On February 4th 2020, Google Chrome will require SameSite=None; to be added to all cross-site cookies. Rails 6.1 and soon Rails 6.0 have added a same_site: :none option to the rails cookie hash:

cookies["foo"]= {
  value: "bar",
  expires: 1.year.from_now,
  same_site: :none
} 

But older Rails 5.x apps won't receive the upgrade to have access to the same_site options hash. I know the SameSite=None; cookie option can be manually added to Rails in a controller using:

response.headers["Set-Cookie"] = "my=cookie; path=/; expires=#{1.year.from_now}; SameSite=None;"

But my Rails 5.x app uses complicated cookie objects that modify cookies. Instead of breaking them apart, I would like to write Rack middleware to manually update all cookies with the SameSite=None; attribute at once.

This StackOverflow answer shows a way to cookies can be modified to update cookies within Rack Middleware:

# lib/same_site_cookie_middleware
class SameSiteCookieMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, body = @app.call(env)
    # confusingly, response takes its args in a different order
    # than rack requires them to be passed on
    # I know it's because most likely you'll modify the body, 
    # and the defaults are fine for the others. But, it still bothers me.

    response = Rack::Response.new body, status, headers

    response.set_cookie("foo", {:value => "bar", :path => "/", :expires => 1.year.from_now, same_site: :none})
    response.finish # finish writes out the response in the expected format.
  end
end
# application.rb
require 'same_site_cookie_middleware'
config.middleware.insert_after(ActionDispatch::Cookies, SameSiteCookieMiddleware)

How do I re-write this Rack Middleware code to manually append SameSite=None; into every existing cookie?

Kelsey Hannan
  • 2,857
  • 2
  • 30
  • 46

6 Answers6

4

I was able to get this to work with the following:

# frozen_string_literals: true

class SameSiteCookies

  def initialize(app)
    @app = app
  end

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

    set_cookie_header = headers['Set-Cookie']

    if set_cookie_header && !(set_cookie_header =~ /SameSite\=/)

      headers['Set-Cookie'] << ';' if !(set_cookie_header =~ /;$/)
      headers['Set-Cookie'] << ' SameSite=None'
      headers['Set-Cookie'] << '; Secure' if env['rack.url_scheme'] == 'https';

    end

    [status, headers, body]
  end
end

and adding to middleware with:

Rails.application.config.middleware.insert_before(ActionDispatch::Cookies, SameSiteCookies)
cesartalves
  • 1,507
  • 9
  • 18
Carson Reinke
  • 713
  • 5
  • 16
  • i created this file in lib folder. now when i tried to use middleware it is giving me error `uninitialized constant Application::SameSiteCookies (NameError)` – Vishal Apr 01 '20 at 07:11
  • Worked for me in a Rails 4.1 app, just create the file in `config/initializers/same_site.rb` – Alexander Malfait Aug 14 '20 at 11:55
4

I was able to get all cookies to use SameSite=None by default updating rack:

gem 'rack', '~> 2.1'

use Rack::Session::Cookie, 
        :httponly     => true,
        :same_site    => :none,
        :secure       => true,
        :secret       => COOKIE_SECRET.to_s()
osowskit
  • 5,904
  • 2
  • 29
  • 38
  • 1
    could you please clarify where this code should be added? – goodniceweb Aug 26 '20 at 20:06
  • 1
    @goodniceweb i have this alongside the rest of my app initialization in `class App < Sinatra::Base` though assume this just needs to be set before handling any requests. One tip though is to disable this in debug/development environments – osowskit Aug 27 '20 at 00:15
3

I had a problem with Rails 5 headers being frozen. This is similar to Carson's answer but it goes around this problem. Should work for both rails 5 < and Rails 5+.

# frozen_string_literals: true

class SameSiteCookies

  def initialize(app)
    @app = app
  end

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

    set_cookie_header = headers['Set-Cookie']

    if set_cookie_header && !(set_cookie_header =~ /SameSite\=/)
      # the set cookie header variable is frozen
      new_set_cookie_header = set_cookie_header.dup
      new_set_cookie_header << ';' if !(set_cookie_header =~ /;$/)
      new_set_cookie_header << ' SameSite=None'
      new_set_cookie_header << '; Secure' if is_ssl?

      headers['Set-Cookie'] = new_set_cookie_header

    end

    [status, headers, body]
  end

  private

  def is_ssl?
    # custom logic for my application
  end
end

Insert the middleware

Rails.application.config.middleware.insert_before(ActionDispatch::Cookies, SameSiteCookies)
cesartalves
  • 1,507
  • 9
  • 18
  • After the chrome rollout of August 11, you should set SameSite=None only if your site is served as HTTPS as well: https://www.chromium.org/updates/same-site – cesartalves Aug 13 '20 at 12:51
1

Update: For Rails 5.x and lower, I found the rails_same_site_cookie gem to be a good option for adding SameSite=None; to all your app's cookies. It uses middleware to do it.

Kelsey Hannan
  • 2,857
  • 2
  • 30
  • 46
  • I've been using this quite some time and works well. Only issue I have is it setting cookies for cross domain requestss. I can see the cookie in the response, but it doesen't appear to get set. Any experience with that? – Matt Feb 05 '21 at 00:32
  • What browser are you having this trouble with? Safari now disables all 3rd party cookies. Chrome is similarly planning to phase them out. Awhile ago I wrote a hack that allows for 3rd parties to set cookies in Safari via 1st party interactions, but it might not work anymore: https://github.com/KelseyDH/storage_access_api_rails – Kelsey Hannan Feb 09 '21 at 00:02
  • I actually got it to work now in chrome, ended up being an issue with setting `Access-Control-Allow-Credentials'` that I resolved using Rack-Cors. I'm working on making a widget that could be used embeded on any host. I'll take a look at that hack provided. I'm starting to wonder if I'll need to take another approach other than 3rd party cookies as chrome is looking to block them in the future as well? – Matt Feb 10 '21 at 01:38
  • @Matt Yes if 3rd party cookies are core to your widget you're going to be in a world of pain as they are being phased out. When I wrote my hack Safari allowed 3rd party cookies if you had visited that site directly, thus the "1st party interactions" hack. Another hack you can do, which sadly requires your client to update their DNS configuration, is to CNAME cloak a subdomain on their site to your server, so that the 3rd party cookie appears set 1st party. The rules are constantly changing around cookies, but as a general rule Safari is taking the most strict approach so test against them. – Kelsey Hannan Feb 10 '21 at 20:18
  • 1
    Thanks Kelsey, appreciate the confirmation on my thoughts. I had used the "1st party interaction" hack previously, but as you stated the rules keep changing and it's a lot to keep up with. I was holding out some hope that I could hack something maintanable together. Part of that hope came from seeing that so much of the web is still depedning on 3rd party cookies and wanted to make sure I wasn't overlooking something. Looks like a lot of sites could be in for a world of pain soon. – Matt Feb 11 '21 at 15:27
1

The secure_headers gem lets you configure the cookie policy out of the box:

SecureHeaders::Configuration.default do |config|
  config.cookies = {
    secure: true, # mark all cookies as "Secure"
    httponly: true, # mark all cookies as "HttpOnly"
    samesite: {
      none: true # mark all cookies as SameSite=lax
    }
  }

I used this solution to add SameSite=None to our cookies on a Rails 5 application.

Andy Ray
  • 30,372
  • 14
  • 101
  • 138
0

Once a cookie is set you cannot modify the cookie properties like expiry, domain, path.

Browsers return only the cookie name and value once a cookie has already been set, over-riding any of the cookie property will create a new cookie. I would recommend to delete the existing cookie and create a new cookie with same name and value.

headers['Set-Cookie'] instructs the browser to create a new cookie and modifying the value in middleware gives you a very little control on the attribute value.

I have answered here how this can be achieved by modifying the Rack::Utils.set_cookie_header! method.

anil.n
  • 509
  • 2
  • 5
  • 17
  • What are your thoughts on the rails_same_site_cookie gem? https://github.com/pschinis/rails_same_site_cookie Ideally I would like to not maintain custom middleware code in my own app but go with community convention. – Kelsey Hannan Feb 03 '20 at 20:29
  • You can use this gem if you are sure that `SameSite` value will always be `None`, however if you need a different value then you may have to change this https://github.com/pschinis/rails_same_site_cookie/blob/master/lib/rails_same_site_cookie/middleware.rb#L29. – anil.n Feb 04 '20 at 06:27
  • I liked this gem, it handles unsupported browsers. However it supports only rails 4.1 and above and my application was running on 3.2.22, tweaked it support lower version https://github.com/n-anil/rails_same_site_cookie/tree/rails-3-2-support – anil.n Feb 07 '20 at 11:03