1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Allow CSRF tokens to be stored outside of session

This commit is contained in:
Andrew Kowpak 2022-03-11 10:53:39 -05:00
parent cfa7284789
commit f2c66ce392
No known key found for this signature in database
GPG key ID: DA8B6924A5B66312
10 changed files with 456 additions and 37 deletions

View file

@ -1,3 +1,11 @@
* Add the ability to use custom logic for storing and retrieving CSRF tokens.
By default, the token will be stored in the session. Custom classes can be
defined to specify arbitrary behaviour, but the ability to store them in
encrypted cookies is built in.
*Andrew Kowpak*
* Make ActionController::Parameters#values cast nested hashes into parameters.
*Gannon McGibbon*

View file

@ -55,6 +55,8 @@ module ActionController # :nodoc:
# Learn more about CSRF attacks and securing your application in the
# {Ruby on Rails Security Guide}[https://guides.rubyonrails.org/security.html].
module RequestForgeryProtection
CSRF_TOKEN = "action_controller.csrf_token"
extend ActiveSupport::Concern
include AbstractController::Helpers
@ -90,6 +92,10 @@ module ActionController # :nodoc:
config_accessor :default_protect_from_forgery
self.default_protect_from_forgery = false
# The strategy to use for storing and retrieving CSRF tokens.
config_accessor :csrf_token_storage_strategy
self.csrf_token_storage_strategy = SessionStore.new
helper_method :form_authenticity_token
helper_method :protect_against_forgery?
end
@ -140,11 +146,39 @@ module ActionController # :nodoc:
# class ApplicationController < ActionController:x:Base
# protect_from_forgery with: CustomStrategy
# end
# * <tt>:store</tt> - Set the strategy to store and retrieve CSRF tokens.
#
# Built-in session token strategies are:
# * <tt>:session</tt> - Store the CSRF token in the session. Used as default if <tt>:store</tt> option is not specified.
# * <tt>:cookie</tt> - Store the CSRF token in an encrypted cookie.
#
# You can also implement custom strategy classes for CSRF token storage:
#
# class CustomStore
# def fetch(request)
# # Return the token from a custom location
# end
#
# def store(request, csrf_token)
# # Store the token in a custom location
# end
#
# def reset(request)
# # Delete the stored session token
# end
# end
#
# class ApplicationController < ActionController:x:Base
# protect_from_forgery store: CustomStore.new
# end
def protect_from_forgery(options = {})
options = options.reverse_merge(prepend: false)
self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
self.request_forgery_protection_token ||= :authenticity_token
self.csrf_token_storage_strategy = storage_strategy(options[:store] || SessionStore.new)
before_action :verify_authenticity_token, options
append_after_action :verify_same_origin_request
end
@ -173,6 +207,22 @@ module ActionController # :nodoc:
raise ArgumentError, "Invalid request forgery protection method, use :null_session, :exception, :reset_session, or a custom forgery protection class."
end
end
def storage_strategy(name)
case name
when :session
SessionStore.new
when :cookie
CookieStore.new(:csrf_token)
else
return name if is_storage_strategy?(name)
raise ArgumentError, "Invalid CSRF token storage strategy, use :session, :cookie, or a custom CSRF token storage class."
end
end
def is_storage_strategy?(object)
object.respond_to?(:fetch) && object.respond_to?(:store)
end
end
module ProtectionMethods
@ -240,6 +290,63 @@ module ActionController # :nodoc:
end
end
class SessionStore
def fetch(request)
request.session[:_csrf_token]
end
def store(request, csrf_token)
request.session[:_csrf_token] = csrf_token
end
def reset(request)
request.session.delete(:_csrf_token)
end
end
class CookieStore
def initialize(cookie = :csrf_token)
@cookie_name = cookie
end
def fetch(request)
contents = request.cookie_jar.encrypted[@cookie_name]
return nil if contents.nil?
value = JSON.parse(contents)
return nil unless value.dig("session_id", "public_id") == request.session.id_was&.public_id
value["token"]
rescue JSON::ParserError
nil
end
def store(request, csrf_token)
request.cookie_jar.encrypted.permanent[@cookie_name] = {
value: {
token: csrf_token,
session_id: request.session.id,
}.to_json,
httponly: true,
same_site: :lax,
}
end
def reset(request)
request.cookie_jar.delete(@cookie_name)
end
end
def reset_csrf_token(request) # :doc:
request.env.delete(CSRF_TOKEN)
csrf_token_storage_strategy.reset(request)
end
def commit_csrf_token(request) # :doc:
csrf_token = request.env[CSRF_TOKEN]
csrf_token_storage_strategy.store(request, csrf_token) unless csrf_token.nil?
end
private
# The actual before_action that is used to verify the CSRF token.
# Don't override this directly. Provide your own forgery protection
@ -341,20 +448,20 @@ module ActionController # :nodoc:
# Creates the authenticity token for the current request.
def form_authenticity_token(form_options: {}) # :doc:
masked_authenticity_token(session, form_options: form_options)
masked_authenticity_token(form_options: form_options)
end
# Creates a masked version of the authenticity token that varies
# on each request. The masking is used to mitigate SSL attacks
# like BREACH.
def masked_authenticity_token(session, form_options: {})
def masked_authenticity_token(form_options: {})
action, method = form_options.values_at(:action, :method)
raw_token = if per_form_csrf_tokens && action && method
action_path = normalize_action_path(action)
per_form_csrf_token(session, action_path, method)
per_form_csrf_token(nil, action_path, method)
else
global_csrf_token(session)
global_csrf_token
end
mask_token(raw_token)
@ -382,14 +489,14 @@ module ActionController # :nodoc:
# This is actually an unmasked token. This is expected if
# you have just upgraded to masked tokens, but should stop
# happening shortly after installing this gem.
compare_with_real_token masked_token, session
compare_with_real_token masked_token
elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
csrf_token = unmask_token(masked_token)
compare_with_global_token(csrf_token, session) ||
compare_with_real_token(csrf_token, session) ||
valid_per_form_csrf_token?(csrf_token, session)
compare_with_global_token(csrf_token) ||
compare_with_real_token(csrf_token) ||
valid_per_form_csrf_token?(csrf_token)
else
false # Token is malformed.
end
@ -410,15 +517,15 @@ module ActionController # :nodoc:
encode_csrf_token(masked_token)
end
def compare_with_real_token(token, session) # :doc:
def compare_with_real_token(token, session = nil) # :doc:
ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, real_csrf_token(session))
end
def compare_with_global_token(token, session) # :doc:
def compare_with_global_token(token, session = nil) # :doc:
ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, global_csrf_token(session))
end
def valid_per_form_csrf_token?(token, session) # :doc:
def valid_per_form_csrf_token?(token, session = nil) # :doc:
if per_form_csrf_tokens
correct_token = per_form_csrf_token(
session,
@ -432,9 +539,12 @@ module ActionController # :nodoc:
end
end
def real_csrf_token(session) # :doc:
session[:_csrf_token] ||= generate_csrf_token
decode_csrf_token(session[:_csrf_token])
def real_csrf_token(_session = nil) # :doc:
csrf_token = request.env.fetch(CSRF_TOKEN) do
request.env[CSRF_TOKEN] = csrf_token_storage_strategy.fetch(request) || generate_csrf_token
end
decode_csrf_token(csrf_token)
end
def per_form_csrf_token(session, action_path, method) # :doc:
@ -444,7 +554,7 @@ module ActionController # :nodoc:
GLOBAL_CSRF_TOKEN_IDENTIFIER = "!real_csrf_token"
private_constant :GLOBAL_CSRF_TOKEN_IDENTIFIER
def global_csrf_token(session) # :doc:
def global_csrf_token(session = nil) # :doc:
csrf_token_hmac(session, GLOBAL_CSRF_TOKEN_IDENTIFIER)
end

View file

@ -182,11 +182,12 @@ module ActionController
class TestSession < Rack::Session::Abstract::PersistedSecure::SecureSessionHash # :nodoc:
DEFAULT_OPTIONS = Rack::Session::Abstract::Persisted::DEFAULT_OPTIONS
def initialize(session = {})
def initialize(session = {}, id = Rack::Session::SessionId.new(SecureRandom.hex(16)))
super(nil, nil)
@id = Rack::Session::SessionId.new(SecureRandom.hex(16))
@id = id
@data = stringify_keys(session)
@loaded = true
@initially_empty = @data.empty?
end
def exists?
@ -218,6 +219,10 @@ module ActionController
true
end
def id_was
@id
end
private
def load!
@id

View file

@ -358,6 +358,7 @@ module ActionDispatch
def reset_session
session.destroy
controller_instance.reset_csrf_token(self) if controller_instance.respond_to?(:reset_csrf_token)
end
def session=(session) # :nodoc:
@ -429,6 +430,10 @@ module ActionDispatch
"#<#{self.class.name} #{method} #{original_url.dump} for #{remote_ip}>"
end
def commit_csrf_token
controller_instance.commit_csrf_token(self) if controller_instance.respond_to?(:commit_csrf_token)
end
private
def check_method(name)
HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS[0...-1].join(', ')}, and #{HTTP_METHODS[-1]}")

View file

@ -67,6 +67,11 @@ module ActionDispatch
end
module SessionObject # :nodoc:
def commit_session(req, res)
req.commit_csrf_token
super(req, res)
end
def prepare_session(req)
Request::Session.create(self, req, @default_options)
end

View file

@ -78,6 +78,8 @@ module ActionDispatch
@loaded = false
@exists = nil # We haven't checked yet.
@enabled = enabled
@id_was = nil
@id_was_initialized = false
end
def id
@ -241,6 +243,11 @@ module ActionDispatch
to_hash.each(&block)
end
def id_was
load_for_read!
@id_was
end
private
def load_for_read!
load! if !loaded? && exists?
@ -260,10 +267,13 @@ module ActionDispatch
def load!
if enabled?
@id_was_initialized = true unless exists?
id, session = @by.load_session @req
options[:id] = id
@delegate.replace(session.stringify_keys)
@id_was = id unless @id_was_initialized
end
@id_was_initialized = true
@loaded = true
end
end

View file

@ -195,6 +195,50 @@ class SkipProtectionWhenUnprotectedController < ActionController::Base
skip_forgery_protection
end
class CookieCsrfTokenStorageStrategyController < ActionController::Base
include RequestForgeryProtectionActions
after_action :commit_token, only: :cookie
protect_from_forgery only: %w(index meta same_origin_js negotiate_same_origin), with: :exception, store: :cookie
def reset
reset_csrf_token(request)
head :ok
end
def cookie
render inline: "<%= csrf_meta_tags %>"
end
private
def commit_token
request.commit_csrf_token
end
end
class CustomCsrfTokenStorageStrategyController < ActionController::Base
include RequestForgeryProtectionActions
class CustomStrategy
def fetch(request)
request.env[:custom_storage]
end
def store(request, csrf_token)
request.env[:custom_storage] = csrf_token
end
def reset(request)
request.env[:custom_storage] = nil
end
end
protect_from_forgery only: %w(index meta same_origin_js negotiate_same_origin),
with: :reset_session,
store: CustomStrategy.new
end
# common test methods
module RequestForgeryProtectionTests
def setup
@ -394,7 +438,7 @@ module RequestForgeryProtectionTests
end
def test_should_allow_post_with_token
session[:_csrf_token] = @token
initialize_csrf_token
@controller.stub :form_authenticity_token, @token do
assert_not_blocked { post :index, params: { custom_authenticity_token: @token } }
end
@ -403,60 +447,60 @@ module RequestForgeryProtectionTests
def test_should_allow_post_with_strict_encoded_token
token_length = (ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH * 4.0 / 3).ceil
token_including_url_unsafe_chars = "+/".ljust(token_length, "A")
session[:_csrf_token] = token_including_url_unsafe_chars
initialize_csrf_token(token_including_url_unsafe_chars)
@controller.stub :form_authenticity_token, token_including_url_unsafe_chars do
assert_not_blocked { post :index, params: { custom_authenticity_token: token_including_url_unsafe_chars } }
end
end
def test_should_allow_patch_with_token
session[:_csrf_token] = @token
initialize_csrf_token
@controller.stub :form_authenticity_token, @token do
assert_not_blocked { patch :index, params: { custom_authenticity_token: @token } }
end
end
def test_should_allow_put_with_token
session[:_csrf_token] = @token
initialize_csrf_token
@controller.stub :form_authenticity_token, @token do
assert_not_blocked { put :index, params: { custom_authenticity_token: @token } }
end
end
def test_should_allow_delete_with_token
session[:_csrf_token] = @token
initialize_csrf_token
@controller.stub :form_authenticity_token, @token do
assert_not_blocked { delete :index, params: { custom_authenticity_token: @token } }
end
end
def test_should_allow_post_with_token_in_header
session[:_csrf_token] = @token
initialize_csrf_token
@request.env["HTTP_X_CSRF_TOKEN"] = @token
assert_not_blocked { post :index }
end
def test_should_allow_delete_with_token_in_header
session[:_csrf_token] = @token
initialize_csrf_token
@request.env["HTTP_X_CSRF_TOKEN"] = @token
assert_not_blocked { delete :index }
end
def test_should_allow_patch_with_token_in_header
session[:_csrf_token] = @token
initialize_csrf_token
@request.env["HTTP_X_CSRF_TOKEN"] = @token
assert_not_blocked { patch :index }
end
def test_should_allow_put_with_token_in_header
session[:_csrf_token] = @token
initialize_csrf_token
@request.env["HTTP_X_CSRF_TOKEN"] = @token
assert_not_blocked { put :index }
end
def test_should_allow_post_with_origin_checking_and_correct_origin
forgery_protection_origin_check do
session[:_csrf_token] = @token
initialize_csrf_token
@controller.stub :form_authenticity_token, @token do
assert_not_blocked do
@request.set_header "HTTP_ORIGIN", "http://test.host"
@ -468,7 +512,7 @@ module RequestForgeryProtectionTests
def test_should_allow_post_with_origin_checking_and_no_origin
forgery_protection_origin_check do
session[:_csrf_token] = @token
initialize_csrf_token
@controller.stub :form_authenticity_token, @token do
assert_not_blocked do
post :index, params: { custom_authenticity_token: @token }
@ -479,7 +523,7 @@ module RequestForgeryProtectionTests
def test_should_raise_for_post_with_null_origin
forgery_protection_origin_check do
session[:_csrf_token] = @token
initialize_csrf_token
@controller.stub :form_authenticity_token, @token do
exception = assert_raises(ActionController::InvalidAuthenticityToken) do
@request.set_header "HTTP_ORIGIN", "null"
@ -496,7 +540,7 @@ module RequestForgeryProtectionTests
ActionController::Base.logger = logger
forgery_protection_origin_check do
session[:_csrf_token] = @token
initialize_csrf_token
@controller.stub :form_authenticity_token, @token do
assert_blocked do
@request.set_header "HTTP_ORIGIN", "http://bad.host"
@ -513,6 +557,7 @@ module RequestForgeryProtectionTests
ActionController::Base.logger = old_logger
end
def test_should_warn_on_missing_csrf_token
old_logger = ActionController::Base.logger
logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
@ -598,7 +643,7 @@ module RequestForgeryProtectionTests
# Allow non-GET requests since GET is all a remote <script> tag can muster.
def test_should_allow_non_get_js_without_xhr_header
session[:_csrf_token] = @token
initialize_csrf_token
assert_cross_origin_not_blocked { post :same_origin_js, params: { custom_authenticity_token: @token } }
assert_cross_origin_not_blocked { post :same_origin_js, params: { format: "js", custom_authenticity_token: @token } }
assert_cross_origin_not_blocked do
@ -623,12 +668,25 @@ module RequestForgeryProtectionTests
end
end
def test_csrf_token_is_not_saved_if_it_is_nil
@controller.commit_csrf_token(@request)
assert_nil fetch_csrf_token
end
def test_should_not_raise_error_if_token_is_not_a_string
assert_blocked do
patch :index, params: { custom_authenticity_token: { foo: "bar" } }
end
end
def initialize_csrf_token(token = @token, session = self.session)
session[:_csrf_token] = token
end
def fetch_csrf_token
session[:_csrf_token]
end
def assert_blocked
session[:something_like_user_id] = 1
yield
@ -637,7 +695,9 @@ module RequestForgeryProtectionTests
end
def assert_not_blocked(&block)
session[:something_like_user_id] = 1
assert_nothing_raised(&block)
assert_equal 1, session[:something_like_user_id]
assert_response :success
end
@ -713,7 +773,7 @@ class RequestForgeryProtectionControllerUsingExceptionTest < ActionController::T
def test_raised_exception_message_explains_why_it_occurred
forgery_protection_origin_check do
session[:_csrf_token] = @token
initialize_csrf_token
@controller.stub :form_authenticity_token, @token do
exception = assert_raises(ActionController::InvalidAuthenticityToken) do
@request.set_header "HTTP_ORIGIN", "http://bad.host"
@ -860,7 +920,7 @@ class PerFormTokensControllerTest < ActionController::TestCase
def test_per_form_token_is_same_size_as_global_token
get :index
expected = ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH
actual = @controller.send(:per_form_csrf_token, session, "/path", "post").size
actual = @controller.send(:per_form_csrf_token, nil, "/path", "post").size
assert_equal expected, actual
end
@ -998,7 +1058,7 @@ class PerFormTokensControllerTest < ActionController::TestCase
unmasked_token = @controller.send(:unmask_token, Base64.urlsafe_decode64(token))
assert_not_equal @controller.send(:real_csrf_token, session), unmasked_token
assert_not_equal @controller.send(:real_csrf_token), unmasked_token
end
def test_returns_hmacd_token
@ -1008,13 +1068,13 @@ class PerFormTokensControllerTest < ActionController::TestCase
unmasked_token = @controller.send(:unmask_token, Base64.urlsafe_decode64(token))
assert_equal @controller.send(:global_csrf_token, session), unmasked_token
assert_equal @controller.send(:global_csrf_token), unmasked_token
end
def test_accepts_old_csrf_token
get :index
non_hmac_token = @controller.send(:mask_token, @controller.send(:real_csrf_token, session))
non_hmac_token = @controller.send(:mask_token, @controller.send(:real_csrf_token))
# This is required because PATH_INFO isn't reset between requests.
@request.env["PATH_INFO"] = "/per_form_tokens/post_one"
@ -1101,7 +1161,7 @@ class PerFormTokensControllerTest < ActionController::TestCase
def assert_matches_session_token_on_server(form_token, method = "post")
actual = @controller.send(:unmask_token, Base64.urlsafe_decode64(form_token))
expected = @controller.send(:per_form_csrf_token, session, "/per_form_tokens/post_one", method)
expected = @controller.send(:per_form_csrf_token, nil, "/per_form_tokens/post_one", method)
assert_equal expected, actual
end
end
@ -1137,3 +1197,169 @@ class SkipProtectionWhenUnprotectedControllerTest < ActionController::TestCase
assert_response :success
end
end
class CookieCsrfTokenStorageStrategyControllerTest < ActionController::TestCase
include RequestForgeryProtectionTests
class TestSession < ActionController::TestSession
attr_reader :id_was
def initialize(id_was)
super()
@id_was = id_was
end
end
class NullSessionDummyKeyGenerator
def generate_key(secret, length = nil)
"03312270731a2ed0d11ed091c2338a06"
end
end
def setup
@request.env[ActionDispatch::Cookies::GENERATOR_KEY] = NullSessionDummyKeyGenerator.new
@request.env[ActionDispatch::Cookies::COOKIES_ROTATIONS] = ActiveSupport::Messages::RotationConfiguration.new
super
end
def test_csrf_token_is_stored_in_cookie
get :cookie
assert_not session.key?(:_csrf_token)
assert cookies.key?(:csrf_token)
end
def test_csrf_token_is_stored_in_custom_cookie
@controller.csrf_token_storage_strategy =
ActionController::RequestForgeryProtection::CookieStore.new(:custom_cookie)
get :cookie
assert_not cookies.key?(:csrf_token)
assert cookies.key?(:custom_cookie)
end
def test_csrf_token_cookie_has_same_site_lax
get :cookie
assert_match "SameSite=Lax", @response.headers["Set-Cookie"]
end
def test_csrf_token_cookie_is_http_only
get :cookie
assert_match "HttpOnly", @response.headers["Set-Cookie"]
end
def test_csrf_token_cookie_is_permanent
get :cookie
assert_match(%r(#{20.years.from_now.utc.year}), @response.headers["Set-Cookie"])
end
def test_reset_csrf_token_deletes_cookie
get :cookie
get :reset
assert_nil cookies[:csrf_token]
end
def test_should_allow_when_session_id_in_cookie_matches_session_id
initialize_csrf_token
@controller.stub :form_authenticity_token, @token do
assert_not_blocked { post :index, params: { custom_authenticity_token: @token } }
end
end
def test_should_not_allow_when_session_id_in_cookie_does_not_match_session_id
initialize_csrf_token(@token, ActionController::TestSession.new)
@controller.stub :form_authenticity_token, @token do
assert_blocked { post :index, params: { custom_authenticity_token: @token } }
end
end
def test_should_allow_when_session_id_in_cookie_and_session_id_are_nil
@request.session = ActionController::TestSession.new({}, nil)
initialize_csrf_token(@token, nil)
@controller.stub :form_authenticity_token, @token do
assert_not_blocked { post :index, params: { custom_authenticity_token: @token } }
end
end
def test_should_not_allow_when_session_id_in_cookie_but_session_id_is_nil
initialize_csrf_token
@request.session = ActionController::TestSession.new({}, nil)
@controller.stub :form_authenticity_token, @token do
assert_blocked { post :index, params: { custom_authenticity_token: @token } }
end
end
def test_should_allow_when_session_id_in_cookie_is_nil_and_session_created_before_token_validation
initialize_csrf_token(@token, nil)
@request.session = TestSession.new(nil)
@controller.stub :form_authenticity_token, @token do
assert_not_blocked { post :index, params: { custom_authenticity_token: @token } }
end
end
def test_should_allow_when_session_id_in_cookie_is_nil_and_session_reset_before_token_validation
initialize_csrf_token
@request.session = TestSession.new(session.id)
@controller.stub :form_authenticity_token, @token do
assert_not_blocked { post :index, params: { custom_authenticity_token: @token } }
end
end
def test_should_not_allow_when_session_id_in_cookie_but_request_made_with_no_session
initialize_csrf_token
@request.session = TestSession.new(nil)
@controller.stub :form_authenticity_token, @token do
assert_blocked { post :index, params: { custom_authenticity_token: @token } }
end
end
def initialize_csrf_token(token = @token, session = self.session)
cookies.encrypted[:csrf_token] = {
value: {
token: token,
session_id: session&.id,
}.to_json,
httponly: true,
same_site: :lax,
}
end
def fetch_csrf_token
contents = request.cookie_jar.encrypted[:csrf_token]
return nil if contents.nil?
value = JSON.parse(contents)
return nil unless value["session_id"]&.fetch("public_id") == request.session.id_was&.public_id
value["token"]
end
def assert_blocked(&block)
assert_raises(ActionController::InvalidAuthenticityToken, &block)
end
def assert_not_blocked(&block)
assert_nothing_raised(&block)
assert_response :success
end
end
class CustomCsrfTokenStorageStrategyControllerTest < ActionController::TestCase
include RequestForgeryProtectionTests
def test_csrf_token_is_stored_in_custom_location
post :index
@controller.commit_csrf_token(@request)
assert_not session.key?(:_csrf_token)
assert_not_nil request.env[:custom_storage]
end
def initialize_csrf_token(token = @token)
request.env[:custom_storage] = token
end
end

View file

@ -130,6 +130,40 @@ module ActionDispatch
assert_nil session.dig("one", :two)
end
def test_id_was_for_new_session_that_does_not_exist
session = Session.create(store_for_session_that_does_not_exist, req, {})
assert_nil session.id_was
end
def test_id_was_for_session_that_does_not_exist_after_writing
session = Session.create(store_for_session_that_does_not_exist, req, {})
session["one"] = "1"
assert_nil session.id_was
end
def test_id_was_for_session_that_does_not_exist_after_destroying
session = Session.create(store_for_session_that_does_not_exist, req, {})
session.destroy
assert_nil session.id_was
end
def test_id_was_for_existing_session
session = Session.create(store, req, {})
assert_equal 1, session.id_was
end
def test_id_was_for_existing_session_after_write
session = Session.create(store, req, {})
session["one"] = "1"
assert_equal 1, session.id_was
end
def test_id_was_for_existing_session_after_destroy
session = Session.create(store, req, {})
session.destroy
assert_equal 1, session.id_was
end
private
def store
Class.new {
@ -146,6 +180,14 @@ module ActionDispatch
def delete_session(env, id, options); 123; end
}.new
end
def store_for_session_that_does_not_exist
Class.new {
def load_session(env); [1, {}]; end
def session_exists?(env); false; end
def delete_session(env, id, options); 123; end
}.new
end
end
class SessionIntegrationTest < ActionDispatch::IntegrationTest

View file

@ -30,6 +30,10 @@ module ActionDispatch
def write_session(env, sid, session, options)
@sessions[sid] = SessionId.new(sid, session)
end
def session_exists?(req)
true
end
end
def test_session_is_set

View file

@ -21,6 +21,10 @@ module ActionDispatch
def write_session(env, sid, session, options)
@sessions[sid] = session
end
def session_exists?(req)
true
end
end
def test_session_is_set