actionpack 7.0.8.1 → 7.2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +94 -500
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +2 -2
  5. data/lib/abstract_controller/asset_paths.rb +2 -0
  6. data/lib/abstract_controller/base.rb +119 -106
  7. data/lib/abstract_controller/caching/fragments.rb +51 -52
  8. data/lib/abstract_controller/caching.rb +2 -0
  9. data/lib/abstract_controller/callbacks.rb +94 -67
  10. data/lib/abstract_controller/collector.rb +6 -6
  11. data/lib/abstract_controller/deprecator.rb +9 -0
  12. data/lib/abstract_controller/error.rb +2 -0
  13. data/lib/abstract_controller/helpers.rb +121 -91
  14. data/lib/abstract_controller/logger.rb +2 -0
  15. data/lib/abstract_controller/railties/routes_helpers.rb +3 -16
  16. data/lib/abstract_controller/rendering.rb +14 -13
  17. data/lib/abstract_controller/translation.rb +12 -30
  18. data/lib/abstract_controller/url_for.rb +9 -5
  19. data/lib/abstract_controller.rb +8 -0
  20. data/lib/action_controller/api/api_rendering.rb +2 -0
  21. data/lib/action_controller/api.rb +78 -73
  22. data/lib/action_controller/base.rb +199 -141
  23. data/lib/action_controller/caching.rb +16 -11
  24. data/lib/action_controller/deprecator.rb +9 -0
  25. data/lib/action_controller/form_builder.rb +21 -16
  26. data/lib/action_controller/log_subscriber.rb +19 -5
  27. data/lib/action_controller/metal/allow_browser.rb +123 -0
  28. data/lib/action_controller/metal/basic_implicit_render.rb +2 -0
  29. data/lib/action_controller/metal/conditional_get.rb +187 -174
  30. data/lib/action_controller/metal/content_security_policy.rb +26 -25
  31. data/lib/action_controller/metal/cookies.rb +4 -2
  32. data/lib/action_controller/metal/data_streaming.rb +65 -54
  33. data/lib/action_controller/metal/default_headers.rb +6 -2
  34. data/lib/action_controller/metal/etag_with_flash.rb +4 -0
  35. data/lib/action_controller/metal/etag_with_template_digest.rb +18 -14
  36. data/lib/action_controller/metal/exceptions.rb +19 -9
  37. data/lib/action_controller/metal/flash.rb +12 -10
  38. data/lib/action_controller/metal/head.rb +20 -16
  39. data/lib/action_controller/metal/helpers.rb +64 -67
  40. data/lib/action_controller/metal/http_authentication.rb +214 -200
  41. data/lib/action_controller/metal/implicit_render.rb +21 -17
  42. data/lib/action_controller/metal/instrumentation.rb +22 -12
  43. data/lib/action_controller/metal/live.rb +125 -92
  44. data/lib/action_controller/metal/logging.rb +6 -4
  45. data/lib/action_controller/metal/mime_responds.rb +151 -142
  46. data/lib/action_controller/metal/parameter_encoding.rb +34 -32
  47. data/lib/action_controller/metal/params_wrapper.rb +58 -58
  48. data/lib/action_controller/metal/permissions_policy.rb +14 -13
  49. data/lib/action_controller/metal/rate_limiting.rb +62 -0
  50. data/lib/action_controller/metal/redirecting.rb +110 -84
  51. data/lib/action_controller/metal/renderers.rb +50 -49
  52. data/lib/action_controller/metal/rendering.rb +103 -82
  53. data/lib/action_controller/metal/request_forgery_protection.rb +279 -161
  54. data/lib/action_controller/metal/rescue.rb +12 -8
  55. data/lib/action_controller/metal/streaming.rb +174 -132
  56. data/lib/action_controller/metal/strong_parameters.rb +598 -473
  57. data/lib/action_controller/metal/testing.rb +2 -0
  58. data/lib/action_controller/metal/url_for.rb +23 -14
  59. data/lib/action_controller/metal.rb +145 -61
  60. data/lib/action_controller/railtie.rb +25 -9
  61. data/lib/action_controller/railties/helpers.rb +2 -0
  62. data/lib/action_controller/renderer.rb +105 -66
  63. data/lib/action_controller/template_assertions.rb +4 -2
  64. data/lib/action_controller/test_case.rb +157 -128
  65. data/lib/action_controller.rb +17 -3
  66. data/lib/action_dispatch/constants.rb +34 -0
  67. data/lib/action_dispatch/deprecator.rb +9 -0
  68. data/lib/action_dispatch/http/cache.rb +28 -29
  69. data/lib/action_dispatch/http/content_disposition.rb +2 -0
  70. data/lib/action_dispatch/http/content_security_policy.rb +69 -49
  71. data/lib/action_dispatch/http/filter_parameters.rb +27 -12
  72. data/lib/action_dispatch/http/filter_redirect.rb +22 -1
  73. data/lib/action_dispatch/http/headers.rb +23 -21
  74. data/lib/action_dispatch/http/mime_negotiation.rb +37 -48
  75. data/lib/action_dispatch/http/mime_type.rb +60 -30
  76. data/lib/action_dispatch/http/mime_types.rb +5 -1
  77. data/lib/action_dispatch/http/parameters.rb +12 -10
  78. data/lib/action_dispatch/http/permissions_policy.rb +32 -34
  79. data/lib/action_dispatch/http/rack_cache.rb +4 -0
  80. data/lib/action_dispatch/http/request.rb +132 -79
  81. data/lib/action_dispatch/http/response.rb +136 -103
  82. data/lib/action_dispatch/http/upload.rb +19 -15
  83. data/lib/action_dispatch/http/url.rb +75 -73
  84. data/lib/action_dispatch/journey/formatter.rb +19 -6
  85. data/lib/action_dispatch/journey/gtg/builder.rb +4 -3
  86. data/lib/action_dispatch/journey/gtg/simulator.rb +2 -0
  87. data/lib/action_dispatch/journey/gtg/transition_table.rb +10 -8
  88. data/lib/action_dispatch/journey/nfa/dot.rb +2 -0
  89. data/lib/action_dispatch/journey/nodes/node.rb +6 -5
  90. data/lib/action_dispatch/journey/parser.rb +4 -3
  91. data/lib/action_dispatch/journey/parser_extras.rb +2 -0
  92. data/lib/action_dispatch/journey/path/pattern.rb +18 -15
  93. data/lib/action_dispatch/journey/route.rb +12 -9
  94. data/lib/action_dispatch/journey/router/utils.rb +16 -15
  95. data/lib/action_dispatch/journey/router.rb +13 -10
  96. data/lib/action_dispatch/journey/routes.rb +6 -4
  97. data/lib/action_dispatch/journey/scanner.rb +4 -2
  98. data/lib/action_dispatch/journey/visitors.rb +2 -0
  99. data/lib/action_dispatch/journey.rb +2 -0
  100. data/lib/action_dispatch/log_subscriber.rb +25 -0
  101. data/lib/action_dispatch/middleware/actionable_exceptions.rb +7 -6
  102. data/lib/action_dispatch/middleware/assume_ssl.rb +27 -0
  103. data/lib/action_dispatch/middleware/callbacks.rb +4 -0
  104. data/lib/action_dispatch/middleware/cookies.rb +192 -194
  105. data/lib/action_dispatch/middleware/debug_exceptions.rb +36 -27
  106. data/lib/action_dispatch/middleware/debug_locks.rb +18 -13
  107. data/lib/action_dispatch/middleware/debug_view.rb +9 -2
  108. data/lib/action_dispatch/middleware/exception_wrapper.rb +181 -27
  109. data/lib/action_dispatch/middleware/executor.rb +9 -1
  110. data/lib/action_dispatch/middleware/flash.rb +65 -46
  111. data/lib/action_dispatch/middleware/host_authorization.rb +22 -17
  112. data/lib/action_dispatch/middleware/public_exceptions.rb +12 -8
  113. data/lib/action_dispatch/middleware/reloader.rb +9 -5
  114. data/lib/action_dispatch/middleware/remote_ip.rb +88 -83
  115. data/lib/action_dispatch/middleware/request_id.rb +15 -8
  116. data/lib/action_dispatch/middleware/server_timing.rb +8 -6
  117. data/lib/action_dispatch/middleware/session/abstract_store.rb +7 -0
  118. data/lib/action_dispatch/middleware/session/cache_store.rb +14 -7
  119. data/lib/action_dispatch/middleware/session/cookie_store.rb +32 -25
  120. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +9 -3
  121. data/lib/action_dispatch/middleware/show_exceptions.rb +42 -28
  122. data/lib/action_dispatch/middleware/ssl.rb +60 -45
  123. data/lib/action_dispatch/middleware/stack.rb +15 -9
  124. data/lib/action_dispatch/middleware/static.rb +40 -34
  125. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +2 -2
  126. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +4 -4
  127. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +8 -1
  128. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +7 -7
  129. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +2 -2
  130. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +17 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +16 -12
  132. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -1
  133. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +3 -3
  134. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +4 -4
  135. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -1
  136. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +1 -1
  137. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +3 -0
  138. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +47 -38
  139. data/lib/action_dispatch/railtie.rb +12 -4
  140. data/lib/action_dispatch/request/session.rb +39 -27
  141. data/lib/action_dispatch/request/utils.rb +10 -3
  142. data/lib/action_dispatch/routing/endpoint.rb +2 -0
  143. data/lib/action_dispatch/routing/inspector.rb +59 -9
  144. data/lib/action_dispatch/routing/mapper.rb +686 -639
  145. data/lib/action_dispatch/routing/polymorphic_routes.rb +70 -61
  146. data/lib/action_dispatch/routing/redirection.rb +52 -38
  147. data/lib/action_dispatch/routing/route_set.rb +106 -62
  148. data/lib/action_dispatch/routing/routes_proxy.rb +16 -19
  149. data/lib/action_dispatch/routing/url_for.rb +131 -122
  150. data/lib/action_dispatch/routing.rb +152 -150
  151. data/lib/action_dispatch/system_test_case.rb +91 -81
  152. data/lib/action_dispatch/system_testing/browser.rb +27 -19
  153. data/lib/action_dispatch/system_testing/driver.rb +16 -22
  154. data/lib/action_dispatch/system_testing/server.rb +2 -0
  155. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +53 -31
  156. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +2 -0
  157. data/lib/action_dispatch/testing/assertion_response.rb +9 -7
  158. data/lib/action_dispatch/testing/assertions/response.rb +36 -26
  159. data/lib/action_dispatch/testing/assertions/routing.rb +203 -95
  160. data/lib/action_dispatch/testing/assertions.rb +5 -1
  161. data/lib/action_dispatch/testing/integration.rb +240 -229
  162. data/lib/action_dispatch/testing/request_encoder.rb +6 -1
  163. data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
  164. data/lib/action_dispatch/testing/test_process.rb +14 -9
  165. data/lib/action_dispatch/testing/test_request.rb +4 -2
  166. data/lib/action_dispatch/testing/test_response.rb +34 -19
  167. data/lib/action_dispatch.rb +52 -21
  168. data/lib/action_pack/gem_version.rb +5 -3
  169. data/lib/action_pack/version.rb +3 -1
  170. data/lib/action_pack.rb +18 -17
  171. metadata +91 -32
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  require "base64"
4
6
  require "active_support/security_utils"
5
7
  require "active_support/core_ext/array/access"
@@ -7,62 +9,63 @@ require "active_support/core_ext/array/access"
7
9
  module ActionController
8
10
  # HTTP Basic, Digest, and Token authentication.
9
11
  module HttpAuthentication
10
- # = HTTP \Basic authentication
12
+ # # HTTP Basic authentication
11
13
  #
12
- # === Simple \Basic example
14
+ # ### Simple Basic example
13
15
  #
14
- # class PostsController < ApplicationController
15
- # http_basic_authenticate_with name: "dhh", password: "secret", except: :index
16
+ # class PostsController < ApplicationController
17
+ # http_basic_authenticate_with name: "dhh", password: "secret", except: :index
16
18
  #
17
- # def index
18
- # render plain: "Everyone can see me!"
19
- # end
19
+ # def index
20
+ # render plain: "Everyone can see me!"
21
+ # end
20
22
  #
21
- # def edit
22
- # render plain: "I'm only accessible if you know the password"
23
+ # def edit
24
+ # render plain: "I'm only accessible if you know the password"
25
+ # end
23
26
  # end
24
- # end
25
27
  #
26
- # === Advanced \Basic example
28
+ # ### Advanced Basic example
27
29
  #
28
- # Here is a more advanced \Basic example where only Atom feeds and the XML API are protected by HTTP authentication.
29
- # The regular HTML interface is protected by a session approach:
30
+ # Here is a more advanced Basic example where only Atom feeds and the XML API
31
+ # are protected by HTTP authentication. The regular HTML interface is protected
32
+ # by a session approach:
30
33
  #
31
- # class ApplicationController < ActionController::Base
32
- # before_action :set_account, :authenticate
34
+ # class ApplicationController < ActionController::Base
35
+ # before_action :set_account, :authenticate
33
36
  #
34
- # private
35
- # def set_account
36
- # @account = Account.find_by(url_name: request.subdomains.first)
37
- # end
37
+ # private
38
+ # def set_account
39
+ # @account = Account.find_by(url_name: request.subdomains.first)
40
+ # end
38
41
  #
39
- # def authenticate
40
- # case request.format
41
- # when Mime[:xml], Mime[:atom]
42
- # if user = authenticate_with_http_basic { |u, p| @account.users.authenticate(u, p) }
43
- # @current_user = user
42
+ # def authenticate
43
+ # case request.format
44
+ # when Mime[:xml], Mime[:atom]
45
+ # if user = authenticate_with_http_basic { |u, p| @account.users.authenticate(u, p) }
46
+ # @current_user = user
47
+ # else
48
+ # request_http_basic_authentication
49
+ # end
44
50
  # else
45
- # request_http_basic_authentication
46
- # end
47
- # else
48
- # if session_authenticated?
49
- # @current_user = @account.users.find(session[:authenticated][:user_id])
50
- # else
51
- # redirect_to(login_url) and return false
51
+ # if session_authenticated?
52
+ # @current_user = @account.users.find(session[:authenticated][:user_id])
53
+ # else
54
+ # redirect_to(login_url) and return false
55
+ # end
52
56
  # end
53
57
  # end
54
- # end
55
- # end
58
+ # end
56
59
  #
57
60
  # In your integration tests, you can do something like this:
58
61
  #
59
- # def test_access_granted_from_xml
60
- # authorization = ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password)
62
+ # def test_access_granted_from_xml
63
+ # authorization = ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password)
61
64
  #
62
- # get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization }
65
+ # get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization }
63
66
  #
64
- # assert_equal 200, status
65
- # end
67
+ # assert_equal 200, status
68
+ # end
66
69
  module Basic
67
70
  extend self
68
71
 
@@ -70,7 +73,7 @@ module ActionController
70
73
  extend ActiveSupport::Concern
71
74
 
72
75
  module ClassMethods
73
- # Enables HTTP \Basic authentication.
76
+ # Enables HTTP Basic authentication.
74
77
  #
75
78
  # See ActionController::HttpAuthentication::Basic for example usage.
76
79
  def http_basic_authenticate_with(name:, password:, realm: nil, **options)
@@ -82,8 +85,8 @@ module ActionController
82
85
 
83
86
  def http_basic_authenticate_or_request_with(name:, password:, realm: nil, message: nil)
84
87
  authenticate_or_request_with_http_basic(realm, message) do |given_name, given_password|
85
- # This comparison uses & so that it doesn't short circuit and
86
- # uses `secure_compare` so that length information isn't leaked.
88
+ # This comparison uses & so that it doesn't short circuit and uses
89
+ # `secure_compare` so that length information isn't leaked.
87
90
  ActiveSupport::SecurityUtils.secure_compare(given_name.to_s, name) &
88
91
  ActiveSupport::SecurityUtils.secure_compare(given_password.to_s, password)
89
92
  end
@@ -140,81 +143,82 @@ module ActionController
140
143
  end
141
144
  end
142
145
 
143
- # = HTTP \Digest authentication
146
+ # # HTTP Digest authentication
144
147
  #
145
- # === Simple \Digest example
148
+ # ### Simple Digest example
146
149
  #
147
- # require "openssl"
148
- # class PostsController < ApplicationController
149
- # REALM = "SuperSecret"
150
- # USERS = {"dhh" => "secret", #plain text password
151
- # "dap" => OpenSSL::Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password
150
+ # require "openssl"
151
+ # class PostsController < ApplicationController
152
+ # REALM = "SuperSecret"
153
+ # USERS = {"dhh" => "secret", #plain text password
154
+ # "dap" => OpenSSL::Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password
152
155
  #
153
- # before_action :authenticate, except: [:index]
156
+ # before_action :authenticate, except: [:index]
154
157
  #
155
- # def index
156
- # render plain: "Everyone can see me!"
157
- # end
158
+ # def index
159
+ # render plain: "Everyone can see me!"
160
+ # end
158
161
  #
159
- # def edit
160
- # render plain: "I'm only accessible if you know the password"
161
- # end
162
+ # def edit
163
+ # render plain: "I'm only accessible if you know the password"
164
+ # end
162
165
  #
163
- # private
164
- # def authenticate
165
- # authenticate_or_request_with_http_digest(REALM) do |username|
166
- # USERS[username]
166
+ # private
167
+ # def authenticate
168
+ # authenticate_or_request_with_http_digest(REALM) do |username|
169
+ # USERS[username]
170
+ # end
167
171
  # end
168
- # end
169
- # end
172
+ # end
170
173
  #
171
- # === Notes
174
+ # ### Notes
172
175
  #
173
- # The +authenticate_or_request_with_http_digest+ block must return the user's password
174
- # or the ha1 digest hash so the framework can appropriately hash to check the user's
175
- # credentials. Returning +nil+ will cause authentication to fail.
176
+ # The `authenticate_or_request_with_http_digest` block must return the user's
177
+ # password or the ha1 digest hash so the framework can appropriately hash to
178
+ # check the user's credentials. Returning `nil` will cause authentication to
179
+ # fail.
176
180
  #
177
- # Storing the ha1 hash: MD5(username:realm:password), is better than storing a plain password. If
178
- # the password file or database is compromised, the attacker would be able to use the ha1 hash to
179
- # authenticate as the user at this +realm+, but would not have the user's password to try using at
180
- # other sites.
181
+ # Storing the ha1 hash: MD5(username:realm:password), is better than storing a
182
+ # plain password. If the password file or database is compromised, the attacker
183
+ # would be able to use the ha1 hash to authenticate as the user at this `realm`,
184
+ # but would not have the user's password to try using at other sites.
181
185
  #
182
- # In rare instances, web servers or front proxies strip authorization headers before
183
- # they reach your application. You can debug this situation by logging all environment
184
- # variables, and check for HTTP_AUTHORIZATION, amongst others.
186
+ # In rare instances, web servers or front proxies strip authorization headers
187
+ # before they reach your application. You can debug this situation by logging
188
+ # all environment variables, and check for HTTP_AUTHORIZATION, amongst others.
185
189
  module Digest
186
190
  extend self
187
191
 
188
192
  module ControllerMethods
189
- # Authenticate using an HTTP \Digest, or otherwise render an HTTP header
190
- # requesting the client to send a \Digest.
193
+ # Authenticate using an HTTP Digest, or otherwise render an HTTP header
194
+ # requesting the client to send a Digest.
191
195
  #
192
196
  # See ActionController::HttpAuthentication::Digest for example usage.
193
197
  def authenticate_or_request_with_http_digest(realm = "Application", message = nil, &password_procedure)
194
198
  authenticate_with_http_digest(realm, &password_procedure) || request_http_digest_authentication(realm, message)
195
199
  end
196
200
 
197
- # Authenticate using an HTTP \Digest. Returns true if authentication is
201
+ # Authenticate using an HTTP Digest. Returns true if authentication is
198
202
  # successful, false otherwise.
199
203
  def authenticate_with_http_digest(realm = "Application", &password_procedure)
200
204
  HttpAuthentication::Digest.authenticate(request, realm, &password_procedure)
201
205
  end
202
206
 
203
- # Render an HTTP header requesting the client to send a \Digest for
207
+ # Render an HTTP header requesting the client to send a Digest for
204
208
  # authentication.
205
209
  def request_http_digest_authentication(realm = "Application", message = nil)
206
210
  HttpAuthentication::Digest.authentication_request(self, realm, message)
207
211
  end
208
212
  end
209
213
 
210
- # Returns false on a valid response, true otherwise.
214
+ # Returns true on a valid response, false otherwise.
211
215
  def authenticate(request, realm, &password_procedure)
212
216
  request.authorization && validate_digest_response(request, realm, &password_procedure)
213
217
  end
214
218
 
215
- # Returns false unless the request credentials response value matches the expected value.
216
- # First try the password as a ha1 digest password. If this fails, then try it as a plain
217
- # text password.
219
+ # Returns false unless the request credentials response value matches the
220
+ # expected value. First try the password as a ha1 digest password. If this
221
+ # fails, then try it as a plain text password.
218
222
  def validate_digest_response(request, realm, &password_procedure)
219
223
  secret_key = secret_token(request)
220
224
  credentials = decode_credentials_header(request)
@@ -237,9 +241,10 @@ module ActionController
237
241
  end
238
242
  end
239
243
 
240
- # Returns the expected response for a request of +http_method+ to +uri+ with the decoded +credentials+ and the expected +password+
241
- # Optional parameter +password_is_ha1+ is set to +true+ by default, since best practice is to store ha1 digest instead
242
- # of a plain-text password.
244
+ # Returns the expected response for a request of `http_method` to `uri` with the
245
+ # decoded `credentials` and the expected `password` Optional parameter
246
+ # `password_is_ha1` is set to `true` by default, since best practice is to store
247
+ # ha1 digest instead of a plain-text password.
243
248
  def expected_response(http_method, uri, credentials, password, password_is_ha1 = true)
244
249
  ha1 = password_is_ha1 ? password : ha1(credentials, password)
245
250
  ha2 = OpenSSL::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(":"))
@@ -288,36 +293,40 @@ module ActionController
288
293
 
289
294
  # Uses an MD5 digest based on time to generate a value to be used only once.
290
295
  #
291
- # A server-specified data string which should be uniquely generated each time a 401 response is made.
292
- # It is recommended that this string be base64 or hexadecimal data.
293
- # Specifically, since the string is passed in the header lines as a quoted string, the double-quote character is not allowed.
296
+ # A server-specified data string which should be uniquely generated each time a
297
+ # 401 response is made. It is recommended that this string be base64 or
298
+ # hexadecimal data. Specifically, since the string is passed in the header lines
299
+ # as a quoted string, the double-quote character is not allowed.
294
300
  #
295
- # The contents of the nonce are implementation dependent.
296
- # The quality of the implementation depends on a good choice.
297
- # A nonce might, for example, be constructed as the base 64 encoding of
301
+ # The contents of the nonce are implementation dependent. The quality of the
302
+ # implementation depends on a good choice. A nonce might, for example, be
303
+ # constructed as the base 64 encoding of
298
304
  #
299
- # time-stamp H(time-stamp ":" ETag ":" private-key)
305
+ # time-stamp H(time-stamp ":" ETag ":" private-key)
300
306
  #
301
- # where time-stamp is a server-generated time or other non-repeating value,
302
- # ETag is the value of the HTTP ETag header associated with the requested entity,
303
- # and private-key is data known only to the server.
304
- # With a nonce of this form a server would recalculate the hash portion after receiving the client authentication header and
305
- # reject the request if it did not match the nonce from that header or
306
- # if the time-stamp value is not recent enough. In this way the server can limit the time of the nonce's validity.
307
- # The inclusion of the ETag prevents a replay request for an updated version of the resource.
308
- # (Note: including the IP address of the client in the nonce would appear to offer the server the ability
309
- # to limit the reuse of the nonce to the same client that originally got it.
310
- # However, that would break proxy farms, where requests from a single user often go through different proxies in the farm.
311
- # Also, IP address spoofing is not that hard.)
307
+ # where time-stamp is a server-generated time or other non-repeating value, ETag
308
+ # is the value of the HTTP ETag header associated with the requested entity, and
309
+ # private-key is data known only to the server. With a nonce of this form a
310
+ # server would recalculate the hash portion after receiving the client
311
+ # authentication header and reject the request if it did not match the nonce
312
+ # from that header or if the time-stamp value is not recent enough. In this way
313
+ # the server can limit the time of the nonce's validity. The inclusion of the
314
+ # ETag prevents a replay request for an updated version of the resource. (Note:
315
+ # including the IP address of the client in the nonce would appear to offer the
316
+ # server the ability to limit the reuse of the nonce to the same client that
317
+ # originally got it. However, that would break proxy farms, where requests from
318
+ # a single user often go through different proxies in the farm. Also, IP address
319
+ # spoofing is not that hard.)
312
320
  #
313
- # An implementation might choose not to accept a previously used nonce or a previously used digest, in order to
314
- # protect against a replay attack. Or, an implementation might choose to use one-time nonces or digests for
315
- # POST, PUT, or PATCH requests, and a time-stamp for GET requests. For more details on the issues involved see Section 4
316
- # of this document.
321
+ # An implementation might choose not to accept a previously used nonce or a
322
+ # previously used digest, in order to protect against a replay attack. Or, an
323
+ # implementation might choose to use one-time nonces or digests for POST, PUT,
324
+ # or PATCH requests, and a time-stamp for GET requests. For more details on the
325
+ # issues involved see Section 4 of this document.
317
326
  #
318
- # The nonce is opaque to the client. Composed of Time, and hash of Time with secret
319
- # key from the Rails session secret generated upon creation of project. Ensures
320
- # the time cannot be modified by client.
327
+ # The nonce is opaque to the client. Composed of Time, and hash of Time with
328
+ # secret key from the Rails session secret generated upon creation of project.
329
+ # Ensures the time cannot be modified by client.
321
330
  def nonce(secret_key, time = Time.now)
322
331
  t = time.to_i
323
332
  hashed = [t, secret_key]
@@ -325,11 +334,10 @@ module ActionController
325
334
  ::Base64.strict_encode64("#{t}:#{digest}")
326
335
  end
327
336
 
328
- # Might want a shorter timeout depending on whether the request
329
- # is a PATCH, PUT, or POST, and if the client is a browser or web service.
330
- # Can be much shorter if the Stale directive is implemented. This would
331
- # allow a user to use new nonce without prompting the user again for their
332
- # username and password.
337
+ # Might want a shorter timeout depending on whether the request is a PATCH, PUT,
338
+ # or POST, and if the client is a browser or web service. Can be much shorter if
339
+ # the Stale directive is implemented. This would allow a user to use new nonce
340
+ # without prompting the user again for their username and password.
333
341
  def validate_nonce(secret_key, request, value, seconds_to_timeout = 5 * 60)
334
342
  return false if value.nil?
335
343
  t = ::Base64.decode64(value).split(":").first.to_i
@@ -342,80 +350,78 @@ module ActionController
342
350
  end
343
351
  end
344
352
 
345
- # = HTTP \Token authentication
353
+ # # HTTP Token authentication
346
354
  #
347
- # === Simple \Token example
355
+ # ### Simple Token example
348
356
  #
349
- # class PostsController < ApplicationController
350
- # TOKEN = "secret"
357
+ # class PostsController < ApplicationController
358
+ # TOKEN = "secret"
351
359
  #
352
- # before_action :authenticate, except: [ :index ]
360
+ # before_action :authenticate, except: [ :index ]
353
361
  #
354
- # def index
355
- # render plain: "Everyone can see me!"
356
- # end
357
- #
358
- # def edit
359
- # render plain: "I'm only accessible if you know the password"
360
- # end
362
+ # def index
363
+ # render plain: "Everyone can see me!"
364
+ # end
361
365
  #
362
- # private
363
- # def authenticate
364
- # authenticate_or_request_with_http_token do |token, options|
365
- # # Compare the tokens in a time-constant manner, to mitigate
366
- # # timing attacks.
367
- # ActiveSupport::SecurityUtils.secure_compare(token, TOKEN)
368
- # end
366
+ # def edit
367
+ # render plain: "I'm only accessible if you know the password"
369
368
  # end
370
- # end
371
369
  #
370
+ # private
371
+ # def authenticate
372
+ # authenticate_or_request_with_http_token do |token, options|
373
+ # # Compare the tokens in a time-constant manner, to mitigate
374
+ # # timing attacks.
375
+ # ActiveSupport::SecurityUtils.secure_compare(token, TOKEN)
376
+ # end
377
+ # end
378
+ # end
372
379
  #
373
- # Here is a more advanced Token example where only Atom feeds and the XML API are protected by HTTP token authentication.
374
- # The regular HTML interface is protected by a session approach:
380
+ # Here is a more advanced Token example where only Atom feeds and the XML API
381
+ # are protected by HTTP token authentication. The regular HTML interface is
382
+ # protected by a session approach:
375
383
  #
376
- # class ApplicationController < ActionController::Base
377
- # before_action :set_account, :authenticate
384
+ # class ApplicationController < ActionController::Base
385
+ # before_action :set_account, :authenticate
378
386
  #
379
- # private
380
- # def set_account
381
- # @account = Account.find_by(url_name: request.subdomains.first)
382
- # end
387
+ # private
388
+ # def set_account
389
+ # @account = Account.find_by(url_name: request.subdomains.first)
390
+ # end
383
391
  #
384
- # def authenticate
385
- # case request.format
386
- # when Mime[:xml], Mime[:atom]
387
- # if user = authenticate_with_http_token { |t, o| @account.users.authenticate(t, o) }
388
- # @current_user = user
392
+ # def authenticate
393
+ # case request.format
394
+ # when Mime[:xml], Mime[:atom]
395
+ # if user = authenticate_with_http_token { |t, o| @account.users.authenticate(t, o) }
396
+ # @current_user = user
397
+ # else
398
+ # request_http_token_authentication
399
+ # end
389
400
  # else
390
- # request_http_token_authentication
391
- # end
392
- # else
393
- # if session_authenticated?
394
- # @current_user = @account.users.find(session[:authenticated][:user_id])
395
- # else
396
- # redirect_to(login_url) and return false
401
+ # if session_authenticated?
402
+ # @current_user = @account.users.find(session[:authenticated][:user_id])
403
+ # else
404
+ # redirect_to(login_url) and return false
405
+ # end
397
406
  # end
398
407
  # end
399
- # end
400
- # end
401
- #
408
+ # end
402
409
  #
403
410
  # In your integration tests, you can do something like this:
404
411
  #
405
- # def test_access_granted_from_xml
406
- # authorization = ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token)
407
- #
408
- # get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization }
412
+ # def test_access_granted_from_xml
413
+ # authorization = ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token)
409
414
  #
410
- # assert_equal 200, status
411
- # end
415
+ # get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization }
412
416
  #
417
+ # assert_equal 200, status
418
+ # end
413
419
  #
414
- # On shared hosts, Apache sometimes doesn't pass authentication headers to
415
- # FCGI instances. If your environment matches this description and you cannot
420
+ # On shared hosts, Apache sometimes doesn't pass authentication headers to FCGI
421
+ # instances. If your environment matches this description and you cannot
416
422
  # authenticate, try this rule in your Apache setup:
417
423
  #
418
- # RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]
424
+ # RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]
419
425
  module Token
420
426
  TOKEN_KEY = "token="
421
427
  TOKEN_REGEX = /^(Token|Bearer)\s+/
@@ -423,16 +429,20 @@ module ActionController
423
429
  extend self
424
430
 
425
431
  module ControllerMethods
426
- # Authenticate using an HTTP Bearer token, or otherwise render an HTTP
427
- # header requesting the client to send a Bearer token.
432
+ # Authenticate using an HTTP Bearer token, or otherwise render an HTTP header
433
+ # requesting the client to send a Bearer token. For the authentication to be
434
+ # considered successful, `login_procedure` must not return a false value.
435
+ # Typically, the authenticated user is returned.
428
436
  #
429
437
  # See ActionController::HttpAuthentication::Token for example usage.
430
438
  def authenticate_or_request_with_http_token(realm = "Application", message = nil, &login_procedure)
431
439
  authenticate_with_http_token(&login_procedure) || request_http_token_authentication(realm, message)
432
440
  end
433
441
 
434
- # Authenticate using an HTTP Bearer token. Returns true if
435
- # authentication is successful, false otherwise.
442
+ # Authenticate using an HTTP Bearer token. Returns the return value of
443
+ # `login_procedure` if a token is found. Returns `nil` if no token is found.
444
+ #
445
+ # See ActionController::HttpAuthentication::Token for example usage.
436
446
  def authenticate_with_http_token(&login_procedure)
437
447
  Token.authenticate(self, &login_procedure)
438
448
  end
@@ -444,19 +454,20 @@ module ActionController
444
454
  end
445
455
  end
446
456
 
447
- # If token Authorization header is present, call the login
448
- # procedure with the present token and options.
457
+ # If token Authorization header is present, call the login procedure with the
458
+ # present token and options.
459
+ #
460
+ # Returns the return value of `login_procedure` if a token is found. Returns
461
+ # `nil` if no token is found.
449
462
  #
450
- # Returns the return value of <tt>login_procedure</tt> if a
451
- # token is found. Returns <tt>nil</tt> if no token is found.
463
+ # #### Parameters
452
464
  #
453
- # ==== Parameters
465
+ # * `controller` - ActionController::Base instance for the current request.
466
+ # * `login_procedure` - Proc to call if a token is present. The Proc should
467
+ # take two arguments:
454
468
  #
455
- # * +controller+ - ActionController::Base instance for the current request.
456
- # * +login_procedure+ - Proc to call if a token is present. The Proc
457
- # should take two arguments:
469
+ # authenticate(controller) { |token, options| ... }
458
470
  #
459
- # authenticate(controller) { |token, options| ... }
460
471
  #
461
472
  def authenticate(controller, &login_procedure)
462
473
  token, options = token_and_options(controller.request)
@@ -465,21 +476,21 @@ module ActionController
465
476
  end
466
477
  end
467
478
 
468
- # Parses the token and options out of the token Authorization header.
469
- # The value for the Authorization header is expected to have the prefix
470
- # <tt>"Token"</tt> or <tt>"Bearer"</tt>. If the header looks like this:
479
+ # Parses the token and options out of the token Authorization header. The value
480
+ # for the Authorization header is expected to have the prefix `"Token"` or
481
+ # `"Bearer"`. If the header looks like this:
482
+ #
483
+ # Authorization: Token token="abc", nonce="def"
471
484
  #
472
- # Authorization: Token token="abc", nonce="def"
485
+ # Then the returned token is `"abc"`, and the options are `{nonce: "def"}`.
473
486
  #
474
- # Then the returned token is <tt>"abc"</tt>, and the options are
475
- # <tt>{nonce: "def"}</tt>.
487
+ # Returns an `Array` of `[String, Hash]` if a token is present. Returns `nil` if
488
+ # no token is found.
476
489
  #
477
- # Returns an +Array+ of <tt>[String, Hash]</tt> if a token is present.
478
- # Returns +nil+ if no token is found.
490
+ # #### Parameters
479
491
  #
480
- # ==== Parameters
492
+ # * `request` - ActionDispatch::Request instance with the current headers.
481
493
  #
482
- # * +request+ - ActionDispatch::Request instance with the current headers.
483
494
  def token_and_options(request)
484
495
  authorization_request = request.authorization.to_s
485
496
  if authorization_request[TOKEN_REGEX]
@@ -492,21 +503,22 @@ module ActionController
492
503
  rewrite_param_values params_array_from raw_params auth
493
504
  end
494
505
 
495
- # Takes +raw_params+ and turns it into an array of parameters.
506
+ # Takes `raw_params` and turns it into an array of parameters.
496
507
  def params_array_from(raw_params)
497
508
  raw_params.map { |param| param.split %r/=(.+)?/ }
498
509
  end
499
510
 
500
- # This removes the <tt>"</tt> characters wrapping the value.
511
+ # This removes the `"` characters wrapping the value.
501
512
  def rewrite_param_values(array_params)
502
513
  array_params.each { |param| (param[1] || +"").gsub! %r/^"|"$/, "" }
503
514
  end
504
515
 
505
- # This method takes an authorization body and splits up the key-value
506
- # pairs by the standardized <tt>:</tt>, <tt>;</tt>, or <tt>\t</tt>
507
- # delimiters defined in +AUTHN_PAIR_DELIMITERS+.
516
+ # This method takes an authorization body and splits up the key-value pairs by
517
+ # the standardized `:`, `;`, or `\t` delimiters defined in
518
+ # `AUTHN_PAIR_DELIMITERS`.
508
519
  def raw_params(auth)
509
- _raw_params = auth.sub(TOKEN_REGEX, "").split(/\s*#{AUTHN_PAIR_DELIMITERS}\s*/)
520
+ _raw_params = auth.sub(TOKEN_REGEX, "").split(AUTHN_PAIR_DELIMITERS).map(&:strip)
521
+ _raw_params.reject!(&:empty?)
510
522
 
511
523
  if !_raw_params.first&.start_with?(TOKEN_KEY)
512
524
  _raw_params[0] = "#{TOKEN_KEY}#{_raw_params.first}"
@@ -519,10 +531,11 @@ module ActionController
519
531
  #
520
532
  # Returns String.
521
533
  #
522
- # ==== Parameters
534
+ # #### Parameters
535
+ #
536
+ # * `token` - String token.
537
+ # * `options` - Optional Hash of the options.
523
538
  #
524
- # * +token+ - String token.
525
- # * +options+ - Optional Hash of the options.
526
539
  def encode_credentials(token, options = {})
527
540
  values = ["#{TOKEN_KEY}#{token.to_s.inspect}"] + options.map do |key, value|
528
541
  "#{key}=#{value.to_s.inspect}"
@@ -534,10 +547,11 @@ module ActionController
534
547
  #
535
548
  # Returns nothing.
536
549
  #
537
- # ==== Parameters
550
+ # #### Parameters
551
+ #
552
+ # * `controller` - ActionController::Base instance for the outgoing response.
553
+ # * `realm` - String realm to use in the header.
538
554
  #
539
- # * +controller+ - ActionController::Base instance for the outgoing response.
540
- # * +realm+ - String realm to use in the header.
541
555
  def authentication_request(controller, realm, message = nil)
542
556
  message ||= "HTTP Token: Access denied.\n"
543
557
  controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"', "")}")