unified_csrf_prevention 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +24 -0
  5. data/Appraisals +17 -0
  6. data/CHANGELOG.md +6 -0
  7. data/Gemfile +5 -0
  8. data/MIT-LICENSE +21 -0
  9. data/README.md +90 -0
  10. data/Rakefile +3 -0
  11. data/lib/unified_csrf_prevention/core.rb +48 -0
  12. data/lib/unified_csrf_prevention/middleware.rb +48 -0
  13. data/lib/unified_csrf_prevention/railtie.rb +22 -0
  14. data/lib/unified_csrf_prevention/request_forgery_protection.rb +68 -0
  15. data/lib/unified_csrf_prevention/version.rb +5 -0
  16. data/lib/unified_csrf_prevention.rb +8 -0
  17. data/spec/controllers/dummy_controller_spec.rb +211 -0
  18. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  19. data/spec/dummy/app/controllers/dummy_controller.rb +11 -0
  20. data/spec/dummy/app/views/dummy/index.html.erb +1 -0
  21. data/spec/dummy/app/views/layouts/application.html.erb +12 -0
  22. data/spec/dummy/config/application.rb +32 -0
  23. data/spec/dummy/config/boot.rb +3 -0
  24. data/spec/dummy/config/environment.rb +5 -0
  25. data/spec/dummy/config/environments/test.rb +44 -0
  26. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  27. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  28. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  29. data/spec/dummy/config/initializers/inflections.rb +16 -0
  30. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  31. data/spec/dummy/config/initializers/session_store.rb +3 -0
  32. data/spec/dummy/config/initializers/to_time_preserves_timezone.rb +10 -0
  33. data/spec/dummy/config/initializers/wrap_parameters.rb +9 -0
  34. data/spec/dummy/config/routes.rb +5 -0
  35. data/spec/dummy/config/secrets.yml +22 -0
  36. data/spec/dummy/config.ru +4 -0
  37. data/spec/dummy/log/.keep +0 -0
  38. data/spec/rails_helper.rb +9 -0
  39. data/spec/spec_helper.rb +22 -0
  40. data/spec/support/shared_context/for_controllers.rb +53 -0
  41. data/spec/support/shared_examples/for_controllers.rb +77 -0
  42. data/spec/support/shared_examples/for_middleware.rb +35 -0
  43. data/spec/xing/csrf_protection/core_spec.rb +100 -0
  44. data/spec/xing/csrf_protection/middleware_spec.rb +44 -0
  45. data/unified_csrf_prevention.gemspec +34 -0
  46. metadata +215 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: efb44eb12723e4bc987a8480023d81806f5802ef
4
+ data.tar.gz: b77f535be345db63c4949fa3ede96d9c52f11768
5
+ SHA512:
6
+ metadata.gz: dec3d84b3db6f629fbb25049e7bbab44fdf769bfad03352c504967c7cba4e76e9b47de0a46af5bd0b968e6fde33932c2002d6a3c9f3e77dda66ce06d3ce33dc7
7
+ data.tar.gz: c72c8c82e6740c1d239da46e1320d83b8a5b3092e8ed0271a444525a375f52106b3db77167295967a8f4f2871717a9b939f98a67e09c8a74a26feb60d7c9c800
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ .bundle
2
+ Gemfile.lock
3
+ *.log
4
+ pkg/
5
+ gemfiles/
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,24 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.3
3
+ Exclude:
4
+ - 'spec/dummy/**/*'
5
+ - 'gems/**/*'
6
+
7
+ Metrics/LineLength:
8
+ Enabled: false
9
+
10
+ Metrics/BlockLength:
11
+ Exclude:
12
+ - 'spec/**/*.rb'
13
+
14
+ Layout/IndentationWidth:
15
+ Enabled: false
16
+
17
+ Layout/ElseAlignment:
18
+ Enabled: false
19
+
20
+ Style/TrailingCommaInLiteral:
21
+ Enabled: false
22
+
23
+ Lint/EndAlignment:
24
+ Enabled: false
data/Appraisals ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ appraise 'rails-4.2' do
4
+ gem 'rails', '~>4.2'
5
+ end
6
+
7
+ appraise 'rails-5.0' do
8
+ gem 'rails', '~>5.0'
9
+ end
10
+
11
+ appraise 'rails-5.1' do
12
+ gem 'rails', '~>5.1'
13
+ end
14
+
15
+ appraise 'rails-5.2' do
16
+ gem 'rails', '~>5.2'
17
+ end
data/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0
4
+
5
+ The first open source version, functionally equivalent to Xing's internal
6
+ xing-csrf_prevention gem v1.0.0.
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017-2018 XING SE
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # Unified CSRF Protection for Rails
2
+
3
+ This gem is a drop-in upgrade for request forgery protection in Rails 4 and 5 with the following benefits:
4
+
5
+ * It is self-healing by design. Whenever a user comes in with an invalid authenticity token, they will have a valid one sent back along with the error response so the next request will succeed. There's no need for the user to reload the page.
6
+ * The CSRF protection is decoupled from the users' sessions that could eventually get wiped out or overwritten, resulting in errors.
7
+ * The approach is framework/language agnostic, so the token generated by one application will be accepted by any other, including non-Rails applications (given they implement the same CSRF protection scheme).
8
+ * The mechanism is stateless from the backend's perspective. The data is stored in the browser's cookies, requiring no backend storage.
9
+ * The solution design was audited by Xing Security team and [cure53](https://cure53.de).
10
+ * It is both React- and jQuery-friendly.
11
+
12
+ Please read the [Cross-application CSRF Prevention specification](https://github.com/xing/cross-application-csrf-prevention) for design and implementation details.
13
+
14
+ ## Configuring Your Application
15
+
16
+ 1. Add the gem to the `Gemfile`:
17
+
18
+ ```ruby
19
+ gem 'unified_csrf_prevention'
20
+ ```
21
+
22
+ 2. Set the shared secret key configuration value for `production`, `preview` and whatever other environment your have:
23
+
24
+ ```ruby
25
+ Rails.application.configure do
26
+ # existing configuration settings
27
+
28
+ config.unified_csrf_prevention_key = '64 random characters'
29
+ end
30
+ ```
31
+
32
+ 3. Replace the unobtrusive scripting adapter that adds the `X-CSRF-Token` header which comes with Rails with this one (assuming jQuery is used):
33
+
34
+ ```js
35
+ $.ajaxPrefilter(function(options) {
36
+ var token = (document.cookie.match(/(?:^|;\s*)csrf_token=([^;]+)/) || [])[1];
37
+
38
+ if (token) {
39
+ options.headers["X-CSRF-Token"] = token;
40
+ }
41
+ });
42
+ ```
43
+
44
+ Important note: **the token must be read from cookies for each and every frontend request the application makes. It is not acceptable to read the token once and store it in some variable, DOM node or in any other form.**
45
+
46
+ In other words, please do exactly what the provided snippet does - for any request read the token from the cookie right before the request is sent. Don't try to cache the token, transfer it from the backend, or optimize out the cookie access, otherwise your application could end up using invalid tokens.
47
+
48
+ If your application uses something different from jQuery to make AJAX calls, please adjust the snippet accordingly. The key parts are running the code before each request is sent, and setting the header with the value read from the cookie. Basically, `$.ajaxPrefilter` and `options.headers` should be replaced with something that works with the library you use instead of jQuery.
49
+
50
+ If your application for some reason has several different ways to send AJAX requests, you need to adjust all of them.
51
+
52
+ ## Usage
53
+
54
+ The gem is seamlessly integrated with Rails' built-in request forgery protection mechanism so there's nothing special to be done on top of the regular `protect_from_forgery` controller setting.
55
+ Authenticity tokens transferred in hidden inputs as well as per-form authenticity tokens introduced in Rails 5 just work out of the box.
56
+ See [Ruby on Rails Security Guide](http://guides.rubyonrails.org/security.html#csrf-countermeasures) for details.
57
+
58
+ ## Testing Controllers with Forgery Protection Enabled
59
+
60
+ Sometimes it's necessary to test the controller code with the actual forgery protection mechanisms enabled (`allow_forgery_protection` overwritten in tests).
61
+ Providing the cookies for requests to make `unified_csrf_prevention` work is a bit of a hassle, so instead it's possible to mock the token validation and thus make the controller accept the supplied token:
62
+
63
+ ```ruby
64
+ describe '#some_action' do
65
+ context 'when requested with valid csrf token' do
66
+ let(:csrf_token) { controller.send(:form_authenticity_token) }
67
+
68
+ before do
69
+ allow(controller).to receive(:valid_token?).with(csrf_token).and_return true
70
+ end
71
+
72
+ it 'executes action' do
73
+ post :some_action, authenticity_token: csrf_token
74
+ expect(response).to be_ok
75
+ end
76
+ end
77
+ end
78
+ ```
79
+
80
+ ## Compatibility
81
+
82
+ The gem is compatible with Rails 4.2, 5.0, 5.1 and 5.2.
83
+
84
+ ## Running Specs
85
+
86
+ ```bash
87
+ rubocop
88
+ appraisal install
89
+ appraisal rspec
90
+ ```
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'openssl'
5
+ require 'securerandom'
6
+
7
+ require 'rails'
8
+ require 'active_support/security_utils'
9
+
10
+ module UnifiedCsrfPrevention
11
+ # Low-level routines and constants
12
+ # See https://github.com/xing/cross-application-csrf-prevention#low-level-implementation-details
13
+ module Core
14
+ TOKEN_COOKIE_NAME = 'csrf_token'
15
+ CHECKSUM_COOKIE_NAME = 'csrf_checksum'
16
+ TOKEN_RACK_ENV_VAR = 'unified_csrf_prevention.token'
17
+
18
+ class << self
19
+ def generate_token
20
+ random_bytes_needed = (ActionController::Base::AUTHENTICITY_TOKEN_LENGTH * 0.75).ceil # Base 64 requires four bytes to store three bytes of data
21
+ random_bytes = SecureRandom.random_bytes(random_bytes_needed)
22
+ encode(random_bytes)[0...ActionController::Base::AUTHENTICITY_TOKEN_LENGTH]
23
+ end
24
+
25
+ def checksum_for(token)
26
+ digest_algorithm = OpenSSL::Digest::SHA256.new
27
+ token_digest = OpenSSL::HMAC.digest(digest_algorithm, shared_secret_key, token)
28
+ encode(token_digest)
29
+ end
30
+
31
+ def valid_token?(token, checksum)
32
+ !token.nil? && !checksum.nil? && ActiveSupport::SecurityUtils.secure_compare(checksum_for(token), checksum)
33
+ end
34
+
35
+ private
36
+
37
+ def shared_secret_key
38
+ Rails.configuration.unified_csrf_prevention_key
39
+ rescue NoMethodError
40
+ raise UnifiedCsrfPrevention::ConfigurationError, 'Configuration setting `unified_csrf_prevention_key` is not defined'
41
+ end
42
+
43
+ def encode(binary_string)
44
+ Base64.urlsafe_encode64(binary_string, padding: false)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'unified_csrf_prevention/core'
4
+
5
+ module UnifiedCsrfPrevention
6
+ # Rack middleware to set the token and checksum cookies
7
+ # See https://github.com/xing/cross-application-csrf-prevention#token-generation
8
+ class Middleware
9
+ def initialize(app)
10
+ @app = app
11
+ end
12
+
13
+ def call(env)
14
+ status, headers, body = @app.call(env)
15
+
16
+ if env.key?(Core::TOKEN_RACK_ENV_VAR)
17
+ token = env[Core::TOKEN_RACK_ENV_VAR]
18
+ set_csrf_cookies!(headers, token)
19
+ Rails.logger.info("Set CSRF token: #{token}")
20
+ end
21
+
22
+ [status, headers, body]
23
+ end
24
+
25
+ private
26
+
27
+ def set_csrf_cookies!(headers, token)
28
+ checksum = Core.checksum_for(token)
29
+
30
+ set_cookie!(headers, Core::TOKEN_COOKIE_NAME, value: token)
31
+ set_cookie!(headers, Core::CHECKSUM_COOKIE_NAME, value: checksum, httponly: true)
32
+ end
33
+
34
+ def set_cookie!(headers, name, data)
35
+ cookie = {
36
+ path: '/',
37
+ secure: secure_cookies?,
38
+ same_site: :strict,
39
+ }.merge(data)
40
+
41
+ Rack::Utils.set_cookie_header!(headers, name, cookie)
42
+ end
43
+
44
+ def secure_cookies?
45
+ Rails.env.production? || Rails.env.preview?
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/railtie'
4
+
5
+ require 'unified_csrf_prevention/middleware'
6
+ require 'unified_csrf_prevention/request_forgery_protection'
7
+
8
+ module UnifiedCsrfPrevention
9
+ # A Railtie to automagically set up the gem's middleware and include
10
+ # the controller concern when the gem is loaded
11
+ class Railtie < Rails::Railtie
12
+ initializer :unified_csrf_prevention_middleware do |app|
13
+ app.middleware.use UnifiedCsrfPrevention::Middleware
14
+ end
15
+
16
+ initializer :unified_csrf_prevention_concern do
17
+ ActiveSupport.on_load :action_controller do
18
+ include UnifiedCsrfPrevention::RequestForgeryProtection
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ require 'unified_csrf_prevention/core'
6
+ require 'unified_csrf_prevention/middleware'
7
+
8
+ module UnifiedCsrfPrevention
9
+ # ApplicationController concern implementing request authenticity validation
10
+ # See https://github.com/xing/cross-application-csrf-prevention#application-action-filter
11
+ module RequestForgeryProtection
12
+ extend ActiveSupport::Concern
13
+
14
+ class_methods do
15
+ def protect_from_forgery(options = {})
16
+ super
17
+ prepend_before_action :setup_csrf_token
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def valid_authenticity_token?(_session, token)
24
+ valid_token?(token) || super
25
+ end
26
+
27
+ def compare_with_real_token(token, _session)
28
+ valid_token?(token)
29
+ end
30
+
31
+ def real_csrf_token(_session)
32
+ csrf_token
33
+ end
34
+
35
+ def setup_csrf_token
36
+ csrf_token
37
+ end
38
+
39
+ def csrf_token
40
+ @csrf_token ||= if valid_token?(existing_token) && token_of_correct_length?(existing_token)
41
+ existing_token
42
+ else
43
+ new_token
44
+ end
45
+ end
46
+
47
+ def new_token
48
+ raise UnifiedCsrfPrevention::ConfigurationError, 'UnifiedCsrfPrevention::Middleware middleware must be used' unless Rails.configuration.middleware.include?(UnifiedCsrfPrevention::Middleware)
49
+ request.env[Core::TOKEN_RACK_ENV_VAR] = Core.generate_token
50
+ end
51
+
52
+ def valid_token?(token)
53
+ Core.valid_token?(token, checksum)
54
+ end
55
+
56
+ def existing_token
57
+ request.cookies[Core::TOKEN_COOKIE_NAME]
58
+ end
59
+
60
+ def checksum
61
+ request.cookies[Core::CHECKSUM_COOKIE_NAME]
62
+ end
63
+
64
+ def token_of_correct_length?(token)
65
+ token.length == ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UnifiedCsrfPrevention
4
+ VERSION = '1.0.0'
5
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'unified_csrf_prevention/version'
4
+ require 'unified_csrf_prevention/railtie'
5
+
6
+ module UnifiedCsrfPrevention
7
+ class ConfigurationError < RuntimeError; end
8
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ describe DummyController, type: :controller do
6
+ let(:new_token) { 'This should be exactly 32 bytes.' }
7
+ let(:short_token) { '😻' }
8
+
9
+ before do
10
+ ActionController::Base.allow_forgery_protection = true # RSpec unconditionally disallows the forgery protection in controller tests
11
+ allow(UnifiedCsrfPrevention::Core).to receive(:generate_token).and_return(new_token)
12
+ end
13
+
14
+ describe '#index' do
15
+ render_views
16
+
17
+ let(:perform_request) do
18
+ get :index
19
+ end
20
+
21
+ context 'when token and checksum cookies not sent' do
22
+ let(:output_token) { new_token }
23
+
24
+ it_behaves_like 'an action that outputs the csrf token'
25
+ it_behaves_like 'an action that outputs the csrf cookies'
26
+ end
27
+
28
+ context 'when token and checksum cookies sent' do
29
+ include_context 'with token and checksum'
30
+
31
+ context 'with a valid token' do
32
+ let(:valid_token?) { true }
33
+ let(:output_token) { token }
34
+
35
+ it_behaves_like 'an action that outputs the csrf token'
36
+ it_behaves_like 'an action that does not output the csrf cookies'
37
+ end
38
+
39
+ context 'with a valid but short token' do
40
+ let(:valid_token?) { true }
41
+ let(:token) { short_token }
42
+ let(:output_token) { new_token }
43
+
44
+ it_behaves_like 'an action that outputs the csrf token'
45
+ it_behaves_like 'an action that outputs the csrf cookies'
46
+ end
47
+
48
+ context 'with an invalid token' do
49
+ let(:valid_token?) { false }
50
+ let(:output_token) { new_token }
51
+
52
+ it_behaves_like 'an action that outputs the csrf token'
53
+ it_behaves_like 'an action that outputs the csrf cookies'
54
+ end
55
+ end
56
+
57
+ context 'when cookies middleware is not installed' do
58
+ before do
59
+ allow(Rails.application.config.middleware).to receive(:include?).with(UnifiedCsrfPrevention::Middleware).and_return(false)
60
+ end
61
+
62
+ it 'raises a configuration error' do
63
+ expect { perform_request }.to raise_error UnifiedCsrfPrevention::ConfigurationError
64
+ end
65
+ end
66
+ end
67
+
68
+ describe '#success' do
69
+ let(:perform_request) do
70
+ get :success
71
+ end
72
+
73
+ context 'when token and checksum cookies not sent' do
74
+ let(:output_token) { new_token }
75
+
76
+ it_behaves_like 'an action that outputs the csrf cookies'
77
+ end
78
+
79
+ context 'when token and checksum cookies sent' do
80
+ include_context 'with token and checksum'
81
+
82
+ context 'with a valid token' do
83
+ let(:valid_token?) { true }
84
+ let(:output_token) { token }
85
+
86
+ it_behaves_like 'an action that does not output the csrf cookies'
87
+ end
88
+
89
+ context 'with an invalid token' do
90
+ let(:valid_token?) { false }
91
+ let(:output_token) { new_token }
92
+
93
+ it_behaves_like 'an action that outputs the csrf cookies'
94
+ end
95
+ end
96
+ end
97
+
98
+ describe '#update' do
99
+ context 'when token and checksum cookies not sent' do
100
+ let(:output_token) { new_token }
101
+
102
+ context 'request has no parameters' do
103
+ include_context 'empty update request'
104
+
105
+ it_behaves_like 'an action that responds with a csrf validation error'
106
+ it_behaves_like 'an action that outputs the csrf cookies'
107
+ end
108
+
109
+ context 'request includes a csrf token form parameter' do
110
+ let(:token) { 'some token out of nowhere' }
111
+ include_context 'update request with a csrf token form parameter'
112
+
113
+ it_behaves_like 'an action that responds with a csrf validation error'
114
+ it_behaves_like 'an action that outputs the csrf cookies'
115
+ end
116
+
117
+ context 'request includes a csrf token header' do
118
+ let(:token) { 'some token out of nowhere' }
119
+ include_context 'update request with a csrf token header'
120
+
121
+ it_behaves_like 'an action that responds with a csrf validation error'
122
+ it_behaves_like 'an action that outputs the csrf cookies'
123
+ end
124
+ end
125
+
126
+ context 'when token and checksum cookies sent' do
127
+ include_context 'with token and checksum'
128
+
129
+ context 'with a valid token' do
130
+ let(:valid_token?) { true }
131
+
132
+ context 'and token is of shorter length' do
133
+ let(:token) { short_token }
134
+ let(:output_token) { new_token }
135
+
136
+ include_context 'update request with a csrf token header'
137
+
138
+ it_behaves_like 'an action that responds with OK'
139
+ it_behaves_like 'an action that outputs the csrf cookies'
140
+ end
141
+
142
+ context 'request has no parameters' do
143
+ include_context 'empty update request'
144
+
145
+ it_behaves_like 'an action that responds with a csrf validation error'
146
+ it_behaves_like 'an action that does not output the csrf cookies'
147
+ end
148
+
149
+ context 'request includes a csrf token form parameter' do
150
+ include_context 'update request with a csrf token form parameter'
151
+
152
+ it_behaves_like 'an action that responds with OK'
153
+ it_behaves_like 'an action that does not output the csrf cookies'
154
+ end
155
+
156
+ context 'request includes a csrf token form parameter which was generated on the backend' do
157
+ include_context 'update request with a fetched csrf token form parameter'
158
+
159
+ it_behaves_like 'an action that responds with OK'
160
+ it_behaves_like 'an action that does not output the csrf cookies'
161
+ end
162
+
163
+ context 'request includes a csrf token header' do
164
+ include_context 'update request with a csrf token header'
165
+
166
+ it_behaves_like 'an action that responds with OK'
167
+ it_behaves_like 'an action that does not output the csrf cookies'
168
+ end
169
+ end
170
+
171
+ context 'with an invalid token' do
172
+ let(:valid_token?) { false }
173
+ let(:output_token) { new_token }
174
+
175
+ context 'request has no parameters' do
176
+ include_context 'empty update request'
177
+
178
+ it_behaves_like 'an action that responds with a csrf validation error'
179
+ it_behaves_like 'an action that outputs the csrf cookies'
180
+ end
181
+
182
+ context 'request includes a csrf form parameter' do
183
+ include_context 'update request with a csrf token form parameter'
184
+
185
+ it_behaves_like 'an action that responds with a csrf validation error'
186
+ it_behaves_like 'an action that outputs the csrf cookies'
187
+ end
188
+
189
+ context 'request includes a csrf token header' do
190
+ include_context 'update request with a csrf token header'
191
+
192
+ it_behaves_like 'an action that responds with a csrf validation error'
193
+ it_behaves_like 'an action that outputs the csrf cookies'
194
+ end
195
+ end
196
+ end
197
+ end
198
+
199
+ describe 'on_load hook' do
200
+ context 'is triggered twice' do
201
+ before do
202
+ ActiveSupport.run_load_hooks(:action_controller, ActionController::Base)
203
+ ActiveSupport.run_load_hooks(:action_controller, ActionController::Base)
204
+ end
205
+
206
+ it 'includes RequestForgeryProtection only ones' do
207
+ expect(ActionController::Base.included_modules.find_all { |m| m == UnifiedCsrfPrevention::RequestForgeryProtection }.length).to be 1
208
+ end
209
+ end
210
+ end
211
+ end