Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New session not persisting in Rails 5+ #172

Open
jebentier opened this issue Feb 12, 2021 · 3 comments
Open

New session not persisting in Rails 5+ #172

jebentier opened this issue Feb 12, 2021 · 3 comments

Comments

@jebentier
Copy link

jebentier commented Feb 12, 2021

I've been working on upgrading the platform at my company from Rails 4.2 to Rails 5.x over the past couple months and ran into a rather interesting issue that I've seen referenced symptomatically a few different places. The most prevalent of the symptoms is around the usage of activerecord-session_store and CSRF tokens. Forms that are using CSRF protection on a user with no pre-existing session, are always coming back as invalid because the session store is losing track of the CSRF token stored in the session.

The issue stems from a difference in functionality between the implementations of get_session_model in legacy_support.rb and active_record_store.rb. When encountering a session that has not been stored yet, the LegacySupport implementation creates the session with the ID that was passed, while the ActiveRecordStore implementation generates a fresh session ID and persists that one. The latter introduces a bug when used with rack, because when invoking commit_session, rack makes the assumption that the data that is returned by write_session is what should be persisted as the value of the cookie. And what is returned by ActiveRecord::SessionStore#write_session is the session id that was passed to it.

In the Rails 5 section below, you'll see that the session ID persisted to the store is not the same as the ID set in the cookie.

Proof of Bug

config/application.rb

config.session_store :active_record_store, key: '_my_session_id', domain: :all, ...
config.session_store.session_class = MySessionStore

Rails 4.2 HTTP Response

$> curl -v http://localhost:3000/login
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> GET /login HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Fri, 12 Feb 2021 14:20:26 GMT
< Connection: close
< X-Frame-Options: SAMEORIGIN
< X-XSS-Protection: 1; mode=block
< X-Content-Type-Options: nosniff
< Content-Type: text/html; charset=utf-8
< ETag: W/"4e70ba65fd20b4e3bdced38609b450e6"
< Cache-Control: max-age=0, private, must-revalidate
< Set-Cookie: theme=generic; path=/; expires=Sat, 12 Feb 2022 14:20:26 -0000
< Set-Cookie: _my_session_id=046121f0fa6d7443ee0f295cae982a5f; path=/; HttpOnly
< X-Request-Id: 9578b098-aee9-4623-a571-f746171bcec2
< X-Runtime: 6.976695
<
<!DOCTYPE HTML>
<html lang="en">
  <body>
    <form class="form" action="/login" accept-charset="UTF-8" method="post">
      <input name="utf8" type="hidden" value="&#x2713;" />
      <input type="hidden" name="authenticity_token" value="H56r+OOLX5fVNSAEzHQDTiaPB5X5osJtdKSu5kvFv99xrT0Qk/F9lp2ZcmV9K2nnbQGIzpKyQMIBhF3G9HracA==" />
      <input type='submit'>Login</input>
    </form>
  </body>
</html>

Of note is the session ID and the CSRF token:

< Set-Cookie: _my_session_id=046121f0fa6d7443ee0f295cae982a5f; path=/; HttpOnly
<input type="hidden" name="authenticity_token" value="H56r+OOLX5fVNSAEzHQDTiaPB5X5osJtdKSu5kvFv99xrT0Qk/F9lp2ZcmV9K2nnbQGIzpKyQMIBhF3G9HracA==" />

Prying into the form submission shows:

[1] pry(main)> request.cookies['_my_session_id'] = "046121f0fa6d7443ee0f295cae982a5f"
[2] pry(main)> MySessionStore.find_by_session_id("046121f0fa6d7443ee0f295cae982a5f")
#<MySessionStore:0x00007fa526ad33d8 @session_id="2c611b98e7ed27adb11f8fd8fdab4136", @data=nil, @marshaled_data="\x04\b{\aI\"\nflash\x06:\x06ET{\aI\"\fdiscard\x06;\x00T[\x06I\"\vnotice\x06;\x00TI\"\fflashes\x06;\x00T{\x06@\nIC:\x1EActiveSupport::SafeBuffer\"\x1DPlease login to continue\a;\x00T:\x0F@html_safeTI\"\x10_csrf_token\x06;\x00FI\"1Ma8O933f4tHrp9MPopNKczMmkFvoDrOoYQRebPKa4KY=\x06;\x00F", @created_at="2021-02-12T14:24:24.000Z", @updated_at="2021-02-12T14:24:26.000Z">
[3] pry(main)> session = session_object.data
[4] pry(main)> valid_authenticity_token?(session, "Hlm78EMCXueapl/WIADB3lRriCK9w3ZZ1F9o8jx4BwYv9rUHPt28NnEBjNmCk4utZ00YeVXNxfG1WzaezuLnoA==")
true

Rails 5.2 HTTP Response

$> curl -v http://localhost:3000/login
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> GET /login HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Fri, 12 Feb 2021 14:35:21 GMT
< Connection: close
< X-Frame-Options: SAMEORIGIN
< X-XSS-Protection: 1; mode=block
< X-Content-Type-Options: nosniff
< X-Download-Options: noopen
< X-Permitted-Cross-Domain-Policies: none
< Referrer-Policy: strict-origin-when-cross-origin
< Content-Type: text/html; charset=utf-8
< ETag: W/"dbc69522004eae58bd7d1665e570d607"
< Cache-Control: max-age=0, private, must-revalidate
< Set-Cookie: theme=generic; path=/; expires=Sat, 12 Feb 2022 14:35:21 GMT
< Set-Cookie: _my_session_id=273d35c3eaf404ee2116a3de32527741; path=/; HttpOnly
< X-Request-Id: 66942476-4b1c-46d6-b80c-e1ed1b4a1df0
< X-Runtime: 2.075418
<
<!DOCTYPE HTML>
<html lang="en">
  <body>
    <form class="form" action="/login" accept-charset="UTF-8" method="post">
      <input name="utf8" type="hidden" value="&#x2713;" />
      <input type="hidden" name="authenticity_token" value="mb9Z/DgmgJUrhwM1L6YQtzImRUuJCy1Sq2lFeJ6aFgVoMUnraFhQarMI4IWK1nS5GxvgBlVSHQgPVuMSTs99bw==" />
      <input type='submit'>Login</input>
    </form>
  </body>
</html>

Of note is the session ID and the CSRF token:

< Set-Cookie: _my_session_id=273d35c3eaf404ee2116a3de32527741; path=/; HttpOnly
<input type="hidden" name="authenticity_token" value="mb9Z/DgmgJUrhwM1L6YQtzImRUuJCy1Sq2lFeJ6aFgVoMUnraFhQarMI4IWK1nS5GxvgBlVSHQgPVuMSTs99bw==" />

Prying into the form submission shows that the session ID set in the cookie doesn't exist:

[1] pry(main)> request.cookies['_my_session_id'] = "273d35c3eaf404ee2116a3de32527741"
[2] pry(main)> MySessionStore.find_by_session_id("273d35c3eaf404ee2116a3de32527741")
nil

In the logs I found that a different session ID was saved to the session store:

Create session host:localhost (3.0ms)            
       INSERT INTO simple_sessions ( session_id, marshaled_data, created_at, updated_at, saml_session_index )
          VALUES (
            '46f63be739d8db33c674e6594ff8416d',
            '\u0004\b{\u0006I\"\u0010_csrf_token\u0006:\u0006EFI\"1bV5Ma2Omu7NBLIa8sxMrmp71FaQkuBXLkCv0DZfhSxE=\u0006;\\0F',
            '2021-02-12 14:35:21',
            '2021-02-12 14:35:21',
            NULL
          )

In the same pry from above, proof that the session saved to the store is valid:

[3] pry(main)> session_object = MySessionStore.find_by_session_id("46f63be739d8db33c674e6594ff8416d")
#<MySessionStore:0x00007fa405e884a0 @session_id="46f63be739d8db33c674e6594ff8416d", @data=nil, @marshaled_data="\x04\b{\x06I\"\x10_csrf_token\x06:\x06EFI\"1bV5Ma2Omu7NBLIa8sxMrmp71FaQkuBXLkCv0DZfhSxE=\x06;\x00F", @created_at="2021-02-12T14:35:21.000Z", @updated_at="2021-02-12T14:35:21.000Z">
[3] pry(main)> session = session_object.data
[4] pry(main)> valid_authenticity_token?(session, "mb9Z/DgmgJUrhwM1L6YQtzImRUuJCy1Sq2lFeJ6aFgVoMUnraFhQarMI4IWK1nS5GxvgBlVSHQgPVuMSTs99bw==")
true

Workaround

By applying the following patch to ActiveRecordStore#get_session_model I was able to resolve this issue and carry on with the upgrade.

_ = ::ActionDispatch::Session::ActiveRecordStore
module ActionDispatch
  module Session
    class ActiveRecordStore
      def get_session_model(request, id)
        logger.silence_logger do
          model = @@session_class.find_by_session_id(id)
          if !model
            id ||= generate_sid # id = generate_sid
            model = @@session_class.new(:session_id => id, :data => {})
            model.save
          end
          if request.env[ENV_SESSION_OPTIONS_KEY][:id].nil?
            request.env[SESSION_RECORD_KEY] = model
          else
            request.env[SESSION_RECORD_KEY] ||= model
          end
          model
        end
      end
    end
  end
end

I would be more than happy to discuss and work with anyone to implement a permanent fix to this.

@alecvn
Copy link

alecvn commented Jun 10, 2021

This is still an issue in rails=6.1.3.2 and activerecord-session_store=2.0.0.

@kirykr
Copy link

kirykr commented Jun 19, 2023

Thank you @jebentier for the workaround solution
For those who use Apartment gem you might want to add below line of code

---
def get_session_model(request, id)
  Apartment::Tenant.switch! request.subdomain #gem 'apartment', '~> 2.2'
---

@vantran-se
Copy link

Thank you @jebentier for the workaround solution For those who use Apartment gem you might want to add below line of code

---
def get_session_model(request, id)
  Apartment::Tenant.switch! request.subdomain #gem 'apartment', '~> 2.2'
---

this issue is from middlewares order

run rake middleware to see middlewares order

use ActiveRecord::Migration::CheckPending
use ActionDispatch::Cookies
use ActionDispatch::Session::ActiveRecordStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use ActionDispatch::PermissionsPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
use ActiveRecord::Middleware::ShardSelector

in my case is ShardSelector behind ActiveRecordStore
if you're using Apartment find apartment middleware that handles switch database

add this line to application.rb and see the magic :D

config.middleware.move_before ActionDispatch::Session::ActiveRecordStore, ActiveRecord::Middleware::ShardSelector

endangurura added a commit to endangurura/activerecord-session_store that referenced this issue Jun 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants