lib/action_dispatch/middleware/session/cookie_store.rb



# frozen_string_literal: true

require "active_support/core_ext/hash/keys"
require "action_dispatch/middleware/session/abstract_store"
require "rack/session/cookie"

module ActionDispatch
  module Session
    # = Action Dispatch Session \CookieStore
    #
    # This cookie-based session store is the \Rails default. It is
    # dramatically faster than the alternatives.
    #
    # Sessions typically contain at most a user ID and flash message; both fit
    # within the 4096 bytes cookie size limit. A +CookieOverflow+ exception is raised if
    # you attempt to store more than 4096 bytes of data.
    #
    # The cookie jar used for storage is automatically configured to be the
    # best possible option given your application's configuration.
    #
    # Your cookies will be encrypted using your application's +secret_key_base+. This
    # goes a step further than signed cookies in that encrypted cookies cannot
    # be altered or read by users. This is the default starting in \Rails 4.
    #
    # Configure your session store in an initializer:
    #
    #   Rails.application.config.session_store :cookie_store, key: '_your_app_session'
    #
    # In the development and test environments your application's +secret_key_base+ is
    # generated by \Rails and stored in a temporary file in <tt>tmp/local_secret.txt</tt>.
    # In all other environments, it is stored encrypted in the
    # <tt>config/credentials.yml.enc</tt> file.
    #
    # If your application was not updated to \Rails 5.2 defaults, the +secret_key_base+
    # will be found in the old <tt>config/secrets.yml</tt> file.
    #
    # Note that changing your +secret_key_base+ will invalidate all existing session.
    # Additionally, you should take care to make sure you are not relying on the
    # ability to decode signed cookies generated by your app in external
    # applications or JavaScript before changing it.
    #
    # Because CookieStore extends +Rack::Session::Abstract::Persisted+, many of the
    # options described there can be used to customize the session cookie that
    # is generated. For example:
    #
    #   Rails.application.config.session_store :cookie_store, expire_after: 14.days
    #
    # would set the session cookie to expire automatically 14 days after creation.
    # Other useful options include <tt>:key</tt>, <tt>:secure</tt>,
    # <tt>:httponly</tt>, and <tt>:same_site</tt>.
    class CookieStore < AbstractSecureStore
      class SessionId < DelegateClass(Rack::Session::SessionId)
        attr_reader :cookie_value

        def initialize(session_id, cookie_value = {})
          super(session_id)
          @cookie_value = cookie_value
        end
      end

      DEFAULT_SAME_SITE = proc { |request| request.cookies_same_site_protection } # :nodoc:

      def initialize(app, options = {})
        options[:cookie_only] = true
        options[:same_site] = DEFAULT_SAME_SITE if !options.key?(:same_site)
        super
      end

      def delete_session(req, session_id, options)
        new_sid = generate_sid unless options[:drop]
        # Reset hash and Assign the new session id
        req.set_header("action_dispatch.request.unsigned_session_cookie", new_sid ? { "session_id" => new_sid.public_id } : {})
        new_sid
      end

      def load_session(req)
        stale_session_check! do
          data = unpacked_cookie_data(req)
          data = persistent_session_id!(data)
          [Rack::Session::SessionId.new(data["session_id"]), data]
        end
      end

      private
        def extract_session_id(req)
          stale_session_check! do
            sid = unpacked_cookie_data(req)["session_id"]
            sid && Rack::Session::SessionId.new(sid)
          end
        end

        def unpacked_cookie_data(req)
          req.fetch_header("action_dispatch.request.unsigned_session_cookie") do |k|
            v = stale_session_check! do
              if data = get_cookie(req)
                data.stringify_keys!
              end
              data || {}
            end
            req.set_header k, v
          end
        end

        def persistent_session_id!(data, sid = nil)
          data ||= {}
          data["session_id"] ||= sid || generate_sid.public_id
          data
        end

        def write_session(req, sid, session_data, options)
          session_data["session_id"] = sid.public_id
          SessionId.new(sid, session_data)
        end

        def set_cookie(request, session_id, cookie)
          cookie_jar(request)[@key] = cookie
        end

        def get_cookie(req)
          cookie_jar(req)[@key]
        end

        def cookie_jar(request)
          request.cookie_jar.signed_or_encrypted
        end
    end
  end
end