actionpack 7.1.3 → 7.2.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +82 -501
- 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 +15 -7
- 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 +198 -126
- 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 +123 -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 +210 -205
- 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 +86 -60
- 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 +18 -9
- data/lib/action_dispatch/http/filter_redirect.rb +22 -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 +31 -24
- 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 -44
- data/lib/action_dispatch/http/rack_cache.rb +2 -0
- data/lib/action_dispatch/http/request.rb +94 -75
- data/lib/action_dispatch/http/response.rb +73 -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 +671 -636
- 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 +39 -16
@@ -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,24 +503,21 @@ 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
|
509
515
|
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
# This method takes an authorization body and splits up the key-value
|
514
|
-
# pairs by the standardized <tt>:</tt>, <tt>;</tt>, or <tt>\t</tt>
|
515
|
-
# 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`.
|
516
519
|
def raw_params(auth)
|
517
|
-
_raw_params = auth.sub(TOKEN_REGEX, "").split(
|
520
|
+
_raw_params = auth.sub(TOKEN_REGEX, "").split(AUTHN_PAIR_DELIMITERS).map(&:strip)
|
518
521
|
_raw_params.reject!(&:empty?)
|
519
522
|
|
520
523
|
if !_raw_params.first&.start_with?(TOKEN_KEY)
|
@@ -528,10 +531,11 @@ module ActionController
|
|
528
531
|
#
|
529
532
|
# Returns String.
|
530
533
|
#
|
531
|
-
#
|
534
|
+
# #### Parameters
|
535
|
+
#
|
536
|
+
# * `token` - String token.
|
537
|
+
# * `options` - Optional Hash of the options.
|
532
538
|
#
|
533
|
-
# * +token+ - String token.
|
534
|
-
# * +options+ - Optional Hash of the options.
|
535
539
|
def encode_credentials(token, options = {})
|
536
540
|
values = ["#{TOKEN_KEY}#{token.to_s.inspect}"] + options.map do |key, value|
|
537
541
|
"#{key}=#{value.to_s.inspect}"
|
@@ -543,10 +547,11 @@ module ActionController
|
|
543
547
|
#
|
544
548
|
# Returns nothing.
|
545
549
|
#
|
546
|
-
#
|
550
|
+
# #### Parameters
|
551
|
+
#
|
552
|
+
# * `controller` - ActionController::Base instance for the outgoing response.
|
553
|
+
# * `realm` - String realm to use in the header.
|
547
554
|
#
|
548
|
-
# * +controller+ - ActionController::Base instance for the outgoing response.
|
549
|
-
# * +realm+ - String realm to use in the header.
|
550
555
|
def authentication_request(controller, realm, message = nil)
|
551
556
|
message ||= "HTTP Token: Access denied.\n"
|
552
557
|
controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"', "")}")
|