actionpack 7.1.3.2 → 7.2.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of actionpack might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +70 -530
- data/lib/abstract_controller/asset_paths.rb +2 -0
- data/lib/abstract_controller/base.rb +102 -98
- data/lib/abstract_controller/caching/fragments.rb +50 -53
- data/lib/abstract_controller/caching.rb +2 -0
- data/lib/abstract_controller/callbacks.rb +66 -64
- data/lib/abstract_controller/collector.rb +6 -6
- data/lib/abstract_controller/deprecator.rb +2 -0
- data/lib/abstract_controller/error.rb +2 -0
- data/lib/abstract_controller/helpers.rb +70 -85
- data/lib/abstract_controller/logger.rb +2 -0
- data/lib/abstract_controller/railties/routes_helpers.rb +2 -0
- data/lib/abstract_controller/rendering.rb +13 -12
- data/lib/abstract_controller/translation.rb +11 -10
- data/lib/abstract_controller/url_for.rb +8 -6
- data/lib/abstract_controller.rb +2 -0
- data/lib/action_controller/api/api_rendering.rb +2 -0
- data/lib/action_controller/api.rb +74 -72
- data/lib/action_controller/base.rb +155 -117
- data/lib/action_controller/caching.rb +15 -12
- data/lib/action_controller/deprecator.rb +2 -0
- data/lib/action_controller/form_builder.rb +20 -17
- data/lib/action_controller/log_subscriber.rb +3 -1
- data/lib/action_controller/metal/allow_browser.rb +119 -0
- data/lib/action_controller/metal/basic_implicit_render.rb +2 -0
- data/lib/action_controller/metal/conditional_get.rb +188 -174
- data/lib/action_controller/metal/content_security_policy.rb +25 -24
- data/lib/action_controller/metal/cookies.rb +4 -2
- data/lib/action_controller/metal/data_streaming.rb +64 -55
- data/lib/action_controller/metal/default_headers.rb +5 -3
- data/lib/action_controller/metal/etag_with_flash.rb +3 -1
- data/lib/action_controller/metal/etag_with_template_digest.rb +17 -15
- data/lib/action_controller/metal/exceptions.rb +11 -9
- data/lib/action_controller/metal/flash.rb +12 -10
- data/lib/action_controller/metal/head.rb +12 -10
- data/lib/action_controller/metal/helpers.rb +63 -55
- data/lib/action_controller/metal/http_authentication.rb +209 -201
- data/lib/action_controller/metal/implicit_render.rb +17 -15
- data/lib/action_controller/metal/instrumentation.rb +15 -12
- data/lib/action_controller/metal/live.rb +113 -107
- data/lib/action_controller/metal/logging.rb +6 -4
- data/lib/action_controller/metal/mime_responds.rb +151 -142
- data/lib/action_controller/metal/parameter_encoding.rb +34 -32
- data/lib/action_controller/metal/params_wrapper.rb +57 -59
- data/lib/action_controller/metal/permissions_policy.rb +13 -12
- data/lib/action_controller/metal/rate_limiting.rb +62 -0
- data/lib/action_controller/metal/redirecting.rb +108 -82
- data/lib/action_controller/metal/renderers.rb +50 -49
- data/lib/action_controller/metal/rendering.rb +103 -75
- data/lib/action_controller/metal/request_forgery_protection.rb +162 -133
- data/lib/action_controller/metal/rescue.rb +11 -9
- data/lib/action_controller/metal/streaming.rb +138 -136
- data/lib/action_controller/metal/strong_parameters.rb +525 -480
- data/lib/action_controller/metal/testing.rb +2 -0
- data/lib/action_controller/metal/url_for.rb +17 -15
- data/lib/action_controller/metal.rb +58 -57
- data/lib/action_controller/railtie.rb +3 -0
- data/lib/action_controller/railties/helpers.rb +2 -0
- data/lib/action_controller/renderer.rb +42 -36
- data/lib/action_controller/template_assertions.rb +4 -2
- data/lib/action_controller/test_case.rb +146 -126
- data/lib/action_controller.rb +10 -3
- data/lib/action_dispatch/constants.rb +2 -0
- data/lib/action_dispatch/deprecator.rb +2 -0
- data/lib/action_dispatch/http/cache.rb +27 -26
- data/lib/action_dispatch/http/content_disposition.rb +2 -0
- data/lib/action_dispatch/http/content_security_policy.rb +44 -38
- data/lib/action_dispatch/http/filter_parameters.rb +9 -5
- data/lib/action_dispatch/http/filter_redirect.rb +15 -1
- data/lib/action_dispatch/http/headers.rb +22 -22
- data/lib/action_dispatch/http/mime_negotiation.rb +30 -41
- data/lib/action_dispatch/http/mime_type.rb +29 -22
- data/lib/action_dispatch/http/mime_types.rb +2 -0
- data/lib/action_dispatch/http/parameters.rb +11 -9
- data/lib/action_dispatch/http/permissions_policy.rb +20 -37
- data/lib/action_dispatch/http/rack_cache.rb +2 -0
- data/lib/action_dispatch/http/request.rb +71 -71
- data/lib/action_dispatch/http/response.rb +61 -61
- data/lib/action_dispatch/http/upload.rb +18 -16
- data/lib/action_dispatch/http/url.rb +75 -73
- data/lib/action_dispatch/journey/formatter.rb +13 -6
- data/lib/action_dispatch/journey/gtg/builder.rb +4 -3
- data/lib/action_dispatch/journey/gtg/simulator.rb +2 -0
- data/lib/action_dispatch/journey/gtg/transition_table.rb +10 -8
- data/lib/action_dispatch/journey/nfa/dot.rb +2 -0
- data/lib/action_dispatch/journey/nodes/node.rb +6 -5
- data/lib/action_dispatch/journey/parser.rb +4 -3
- data/lib/action_dispatch/journey/parser_extras.rb +2 -0
- data/lib/action_dispatch/journey/path/pattern.rb +4 -1
- data/lib/action_dispatch/journey/route.rb +9 -7
- data/lib/action_dispatch/journey/router/utils.rb +16 -15
- data/lib/action_dispatch/journey/router.rb +4 -2
- data/lib/action_dispatch/journey/routes.rb +4 -2
- data/lib/action_dispatch/journey/scanner.rb +4 -2
- data/lib/action_dispatch/journey/visitors.rb +2 -0
- data/lib/action_dispatch/journey.rb +2 -0
- data/lib/action_dispatch/log_subscriber.rb +2 -0
- data/lib/action_dispatch/middleware/actionable_exceptions.rb +2 -0
- data/lib/action_dispatch/middleware/assume_ssl.rb +8 -5
- data/lib/action_dispatch/middleware/callbacks.rb +3 -1
- data/lib/action_dispatch/middleware/cookies.rb +119 -104
- data/lib/action_dispatch/middleware/debug_exceptions.rb +13 -5
- data/lib/action_dispatch/middleware/debug_locks.rb +15 -13
- data/lib/action_dispatch/middleware/debug_view.rb +2 -0
- data/lib/action_dispatch/middleware/exception_wrapper.rb +6 -11
- data/lib/action_dispatch/middleware/executor.rb +8 -0
- data/lib/action_dispatch/middleware/flash.rb +63 -51
- data/lib/action_dispatch/middleware/host_authorization.rb +17 -15
- data/lib/action_dispatch/middleware/public_exceptions.rb +8 -6
- data/lib/action_dispatch/middleware/reloader.rb +5 -3
- data/lib/action_dispatch/middleware/remote_ip.rb +77 -72
- data/lib/action_dispatch/middleware/request_id.rb +14 -9
- data/lib/action_dispatch/middleware/server_timing.rb +4 -2
- data/lib/action_dispatch/middleware/session/abstract_store.rb +2 -0
- data/lib/action_dispatch/middleware/session/cache_store.rb +13 -8
- data/lib/action_dispatch/middleware/session/cookie_store.rb +27 -26
- data/lib/action_dispatch/middleware/session/mem_cache_store.rb +7 -3
- data/lib/action_dispatch/middleware/show_exceptions.rb +31 -21
- data/lib/action_dispatch/middleware/ssl.rb +43 -40
- data/lib/action_dispatch/middleware/stack.rb +11 -10
- data/lib/action_dispatch/middleware/static.rb +33 -31
- data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +1 -1
- data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +1 -1
- data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +1 -1
- data/lib/action_dispatch/railtie.rb +2 -4
- data/lib/action_dispatch/request/session.rb +23 -21
- data/lib/action_dispatch/request/utils.rb +2 -0
- data/lib/action_dispatch/routing/endpoint.rb +2 -0
- data/lib/action_dispatch/routing/inspector.rb +5 -3
- data/lib/action_dispatch/routing/mapper.rb +670 -635
- data/lib/action_dispatch/routing/polymorphic_routes.rb +69 -62
- data/lib/action_dispatch/routing/redirection.rb +37 -32
- data/lib/action_dispatch/routing/route_set.rb +59 -45
- data/lib/action_dispatch/routing/routes_proxy.rb +6 -4
- data/lib/action_dispatch/routing/url_for.rb +130 -125
- data/lib/action_dispatch/routing.rb +150 -148
- data/lib/action_dispatch/system_test_case.rb +91 -81
- data/lib/action_dispatch/system_testing/browser.rb +10 -3
- data/lib/action_dispatch/system_testing/driver.rb +3 -1
- data/lib/action_dispatch/system_testing/server.rb +2 -0
- data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +32 -21
- data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +2 -0
- data/lib/action_dispatch/testing/assertion_response.rb +8 -6
- data/lib/action_dispatch/testing/assertions/response.rb +26 -23
- data/lib/action_dispatch/testing/assertions/routing.rb +153 -84
- data/lib/action_dispatch/testing/assertions.rb +2 -0
- data/lib/action_dispatch/testing/integration.rb +223 -222
- data/lib/action_dispatch/testing/request_encoder.rb +2 -0
- data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
- data/lib/action_dispatch/testing/test_process.rb +12 -8
- data/lib/action_dispatch/testing/test_request.rb +3 -1
- data/lib/action_dispatch/testing/test_response.rb +27 -26
- data/lib/action_dispatch.rb +22 -28
- data/lib/action_pack/gem_version.rb +6 -4
- data/lib/action_pack/version.rb +3 -1
- data/lib/action_pack.rb +17 -16
- metadata +30 -13
@@ -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
|
-
#
|
12
|
+
# # HTTP Basic authentication
|
11
13
|
#
|
12
|
-
#
|
14
|
+
# ### Simple Basic example
|
13
15
|
#
|
14
|
-
#
|
15
|
-
#
|
16
|
+
# class PostsController < ApplicationController
|
17
|
+
# http_basic_authenticate_with name: "dhh", password: "secret", except: :index
|
16
18
|
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
19
|
+
# def index
|
20
|
+
# render plain: "Everyone can see me!"
|
21
|
+
# end
|
20
22
|
#
|
21
|
-
#
|
22
|
-
#
|
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
|
-
#
|
28
|
+
# ### Advanced Basic example
|
27
29
|
#
|
28
|
-
# Here is a more advanced
|
29
|
-
# The regular HTML interface is protected
|
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
|
-
#
|
32
|
-
#
|
34
|
+
# class ApplicationController < ActionController::Base
|
35
|
+
# before_action :set_account, :authenticate
|
33
36
|
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
37
|
+
# private
|
38
|
+
# def set_account
|
39
|
+
# @account = Account.find_by(url_name: request.subdomains.first)
|
40
|
+
# end
|
38
41
|
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
#
|
47
|
-
# else
|
48
|
-
# if session_authenticated?
|
49
|
-
# @current_user = @account.users.find(session[:authenticated][:user_id])
|
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
|
50
50
|
# else
|
51
|
-
#
|
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
|
-
#
|
55
|
-
# end
|
58
|
+
# end
|
56
59
|
#
|
57
60
|
# In your integration tests, you can do something like this:
|
58
61
|
#
|
59
|
-
#
|
60
|
-
#
|
62
|
+
# def test_access_granted_from_xml
|
63
|
+
# authorization = ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password)
|
61
64
|
#
|
62
|
-
#
|
65
|
+
# get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization }
|
63
66
|
#
|
64
|
-
#
|
65
|
-
#
|
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
|
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
|
-
#
|
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,67 +143,68 @@ module ActionController
|
|
140
143
|
end
|
141
144
|
end
|
142
145
|
|
143
|
-
#
|
146
|
+
# # HTTP Digest authentication
|
144
147
|
#
|
145
|
-
#
|
148
|
+
# ### Simple Digest example
|
146
149
|
#
|
147
|
-
#
|
148
|
-
#
|
149
|
-
#
|
150
|
-
#
|
151
|
-
#
|
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
|
-
#
|
156
|
+
# before_action :authenticate, except: [:index]
|
154
157
|
#
|
155
|
-
#
|
156
|
-
#
|
157
|
-
#
|
158
|
+
# def index
|
159
|
+
# render plain: "Everyone can see me!"
|
160
|
+
# end
|
158
161
|
#
|
159
|
-
#
|
160
|
-
#
|
161
|
-
#
|
162
|
+
# def edit
|
163
|
+
# render plain: "I'm only accessible if you know the password"
|
164
|
+
# end
|
162
165
|
#
|
163
|
-
#
|
164
|
-
#
|
165
|
-
#
|
166
|
-
#
|
166
|
+
# private
|
167
|
+
# def authenticate
|
168
|
+
# authenticate_or_request_with_http_digest(REALM) do |username|
|
169
|
+
# USERS[username]
|
170
|
+
# end
|
167
171
|
# end
|
168
|
-
#
|
169
|
-
# end
|
172
|
+
# end
|
170
173
|
#
|
171
|
-
#
|
174
|
+
# ### Notes
|
172
175
|
#
|
173
|
-
# The
|
174
|
-
# or the ha1 digest hash so the framework can appropriately hash to
|
175
|
-
# credentials. Returning
|
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
|
178
|
-
# the password file or database is compromised, the attacker
|
179
|
-
#
|
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
|
183
|
-
# they reach your application. You can debug this situation by logging
|
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
|
190
|
-
# requesting the client to send a
|
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
|
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
|
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)
|
@@ -212,9 +216,9 @@ module ActionController
|
|
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
|
216
|
-
# First try the password as a ha1 digest password. If this
|
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
|
241
|
-
#
|
242
|
-
#
|
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
|
292
|
-
# It is recommended that this string be base64 or
|
293
|
-
# Specifically, since the string is passed in the header lines
|
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
|
-
#
|
297
|
-
#
|
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
|
-
#
|
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
|
-
#
|
303
|
-
#
|
304
|
-
#
|
305
|
-
# reject the request if it did not match the nonce
|
306
|
-
# if the time-stamp value is not recent enough. In this way
|
307
|
-
#
|
308
|
-
#
|
309
|
-
#
|
310
|
-
#
|
311
|
-
#
|
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
|
314
|
-
# protect against a replay attack. Or, an
|
315
|
-
#
|
316
|
-
#
|
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
|
319
|
-
# key from the
|
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
|
-
#
|
330
|
-
#
|
331
|
-
#
|
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
|
-
#
|
353
|
+
# # HTTP Token authentication
|
346
354
|
#
|
347
|
-
#
|
355
|
+
# ### Simple Token example
|
348
356
|
#
|
349
|
-
#
|
350
|
-
#
|
357
|
+
# class PostsController < ApplicationController
|
358
|
+
# TOKEN = "secret"
|
351
359
|
#
|
352
|
-
#
|
353
|
-
#
|
354
|
-
# def index
|
355
|
-
# render plain: "Everyone can see me!"
|
356
|
-
# end
|
360
|
+
# before_action :authenticate, except: [ :index ]
|
357
361
|
#
|
358
|
-
#
|
359
|
-
#
|
360
|
-
#
|
362
|
+
# def index
|
363
|
+
# render plain: "Everyone can see me!"
|
364
|
+
# end
|
361
365
|
#
|
362
|
-
#
|
363
|
-
#
|
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
|
374
|
-
# The regular HTML interface is
|
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
|
-
#
|
377
|
-
#
|
384
|
+
# class ApplicationController < ActionController::Base
|
385
|
+
# before_action :set_account, :authenticate
|
378
386
|
#
|
379
|
-
#
|
380
|
-
#
|
381
|
-
#
|
382
|
-
#
|
387
|
+
# private
|
388
|
+
# def set_account
|
389
|
+
# @account = Account.find_by(url_name: request.subdomains.first)
|
390
|
+
# end
|
383
391
|
#
|
384
|
-
#
|
385
|
-
#
|
386
|
-
#
|
387
|
-
#
|
388
|
-
#
|
389
|
-
#
|
390
|
-
#
|
391
|
-
#
|
392
|
-
# else
|
393
|
-
# if session_authenticated?
|
394
|
-
# @current_user = @account.users.find(session[:authenticated][:user_id])
|
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
|
395
400
|
# else
|
396
|
-
#
|
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
|
-
#
|
400
|
-
# end
|
401
|
-
#
|
408
|
+
# end
|
402
409
|
#
|
403
410
|
# In your integration tests, you can do something like this:
|
404
411
|
#
|
405
|
-
#
|
406
|
-
#
|
412
|
+
# def test_access_granted_from_xml
|
413
|
+
# authorization = ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token)
|
407
414
|
#
|
408
|
-
#
|
409
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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,19 +429,18 @@ 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
|
-
#
|
428
|
-
#
|
429
|
-
#
|
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` should return a non-nil value.
|
435
|
+
# Typically, the authenticated user is returned.
|
430
436
|
#
|
431
437
|
# See ActionController::HttpAuthentication::Token for example usage.
|
432
438
|
def authenticate_or_request_with_http_token(realm = "Application", message = nil, &login_procedure)
|
433
439
|
authenticate_with_http_token(&login_procedure) || request_http_token_authentication(realm, message)
|
434
440
|
end
|
435
441
|
|
436
|
-
# Authenticate using an HTTP Bearer token.
|
437
|
-
#
|
438
|
-
# token is found. Returns +nil+ if no token is found.
|
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.
|
439
444
|
#
|
440
445
|
# See ActionController::HttpAuthentication::Token for example usage.
|
441
446
|
def authenticate_with_http_token(&login_procedure)
|
@@ -449,19 +454,20 @@ module ActionController
|
|
449
454
|
end
|
450
455
|
end
|
451
456
|
|
452
|
-
# If token Authorization header is present, call the login
|
453
|
-
#
|
457
|
+
# If token Authorization header is present, call the login procedure with the
|
458
|
+
# present token and options.
|
454
459
|
#
|
455
|
-
# Returns the return value of
|
456
|
-
#
|
460
|
+
# Returns the return value of `login_procedure` if a token is found. Returns
|
461
|
+
# `nil` if no token is found.
|
457
462
|
#
|
458
|
-
#
|
463
|
+
# #### Parameters
|
459
464
|
#
|
460
|
-
# *
|
461
|
-
# *
|
462
|
-
#
|
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:
|
468
|
+
#
|
469
|
+
# authenticate(controller) { |token, options| ... }
|
463
470
|
#
|
464
|
-
# authenticate(controller) { |token, options| ... }
|
465
471
|
#
|
466
472
|
def authenticate(controller, &login_procedure)
|
467
473
|
token, options = token_and_options(controller.request)
|
@@ -470,21 +476,21 @@ module ActionController
|
|
470
476
|
end
|
471
477
|
end
|
472
478
|
|
473
|
-
# Parses the token and options out of the token Authorization header.
|
474
|
-
#
|
475
|
-
#
|
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"
|
476
484
|
#
|
477
|
-
#
|
485
|
+
# Then the returned token is `"abc"`, and the options are `{nonce: "def"}`.
|
478
486
|
#
|
479
|
-
#
|
480
|
-
#
|
487
|
+
# Returns an `Array` of `[String, Hash]` if a token is present. Returns `nil` if
|
488
|
+
# no token is found.
|
481
489
|
#
|
482
|
-
#
|
483
|
-
# Returns +nil+ if no token is found.
|
490
|
+
# #### Parameters
|
484
491
|
#
|
485
|
-
#
|
492
|
+
# * `request` - ActionDispatch::Request instance with the current headers.
|
486
493
|
#
|
487
|
-
# * +request+ - ActionDispatch::Request instance with the current headers.
|
488
494
|
def token_and_options(request)
|
489
495
|
authorization_request = request.authorization.to_s
|
490
496
|
if authorization_request[TOKEN_REGEX]
|
@@ -497,12 +503,12 @@ module ActionController
|
|
497
503
|
rewrite_param_values params_array_from raw_params auth
|
498
504
|
end
|
499
505
|
|
500
|
-
# Takes
|
506
|
+
# Takes `raw_params` and turns it into an array of parameters.
|
501
507
|
def params_array_from(raw_params)
|
502
508
|
raw_params.map { |param| param.split %r/=(.+)?/ }
|
503
509
|
end
|
504
510
|
|
505
|
-
# This removes the
|
511
|
+
# This removes the `"` characters wrapping the value.
|
506
512
|
def rewrite_param_values(array_params)
|
507
513
|
array_params.each { |param| (param[1] || +"").gsub! %r/^"|"$/, "" }
|
508
514
|
end
|
@@ -510,9 +516,9 @@ module ActionController
|
|
510
516
|
WHITESPACED_AUTHN_PAIR_DELIMITERS = /\s*#{AUTHN_PAIR_DELIMITERS}\s*/
|
511
517
|
private_constant :WHITESPACED_AUTHN_PAIR_DELIMITERS
|
512
518
|
|
513
|
-
# This method takes an authorization body and splits up the key-value
|
514
|
-
#
|
515
|
-
#
|
519
|
+
# This method takes an authorization body and splits up the key-value pairs by
|
520
|
+
# the standardized `:`, `;`, or `\t` delimiters defined in
|
521
|
+
# `AUTHN_PAIR_DELIMITERS`.
|
516
522
|
def raw_params(auth)
|
517
523
|
_raw_params = auth.sub(TOKEN_REGEX, "").split(WHITESPACED_AUTHN_PAIR_DELIMITERS)
|
518
524
|
_raw_params.reject!(&:empty?)
|
@@ -528,10 +534,11 @@ module ActionController
|
|
528
534
|
#
|
529
535
|
# Returns String.
|
530
536
|
#
|
531
|
-
#
|
537
|
+
# #### Parameters
|
538
|
+
#
|
539
|
+
# * `token` - String token.
|
540
|
+
# * `options` - Optional Hash of the options.
|
532
541
|
#
|
533
|
-
# * +token+ - String token.
|
534
|
-
# * +options+ - Optional Hash of the options.
|
535
542
|
def encode_credentials(token, options = {})
|
536
543
|
values = ["#{TOKEN_KEY}#{token.to_s.inspect}"] + options.map do |key, value|
|
537
544
|
"#{key}=#{value.to_s.inspect}"
|
@@ -543,10 +550,11 @@ module ActionController
|
|
543
550
|
#
|
544
551
|
# Returns nothing.
|
545
552
|
#
|
546
|
-
#
|
553
|
+
# #### Parameters
|
554
|
+
#
|
555
|
+
# * `controller` - ActionController::Base instance for the outgoing response.
|
556
|
+
# * `realm` - String realm to use in the header.
|
547
557
|
#
|
548
|
-
# * +controller+ - ActionController::Base instance for the outgoing response.
|
549
|
-
# * +realm+ - String realm to use in the header.
|
550
558
|
def authentication_request(controller, realm, message = nil)
|
551
559
|
message ||= "HTTP Token: Access denied.\n"
|
552
560
|
controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"', "")}")
|