omg-actionpack 8.0.0.alpha1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +129 -0
- data/MIT-LICENSE +21 -0
- data/README.rdoc +57 -0
- data/lib/abstract_controller/asset_paths.rb +14 -0
- data/lib/abstract_controller/base.rb +299 -0
- data/lib/abstract_controller/caching/fragments.rb +149 -0
- data/lib/abstract_controller/caching.rb +68 -0
- data/lib/abstract_controller/callbacks.rb +265 -0
- data/lib/abstract_controller/collector.rb +44 -0
- data/lib/abstract_controller/deprecator.rb +9 -0
- data/lib/abstract_controller/error.rb +8 -0
- data/lib/abstract_controller/helpers.rb +243 -0
- data/lib/abstract_controller/logger.rb +16 -0
- data/lib/abstract_controller/railties/routes_helpers.rb +25 -0
- data/lib/abstract_controller/rendering.rb +126 -0
- data/lib/abstract_controller/translation.rb +42 -0
- data/lib/abstract_controller/url_for.rb +37 -0
- data/lib/abstract_controller.rb +36 -0
- data/lib/action_controller/api/api_rendering.rb +18 -0
- data/lib/action_controller/api.rb +155 -0
- data/lib/action_controller/base.rb +332 -0
- data/lib/action_controller/caching.rb +49 -0
- data/lib/action_controller/deprecator.rb +9 -0
- data/lib/action_controller/form_builder.rb +55 -0
- data/lib/action_controller/log_subscriber.rb +96 -0
- data/lib/action_controller/metal/allow_browser.rb +123 -0
- data/lib/action_controller/metal/basic_implicit_render.rb +17 -0
- data/lib/action_controller/metal/conditional_get.rb +341 -0
- data/lib/action_controller/metal/content_security_policy.rb +86 -0
- data/lib/action_controller/metal/cookies.rb +20 -0
- data/lib/action_controller/metal/data_streaming.rb +154 -0
- data/lib/action_controller/metal/default_headers.rb +21 -0
- data/lib/action_controller/metal/etag_with_flash.rb +22 -0
- data/lib/action_controller/metal/etag_with_template_digest.rb +59 -0
- data/lib/action_controller/metal/exceptions.rb +106 -0
- data/lib/action_controller/metal/flash.rb +67 -0
- data/lib/action_controller/metal/head.rb +67 -0
- data/lib/action_controller/metal/helpers.rb +129 -0
- data/lib/action_controller/metal/http_authentication.rb +565 -0
- data/lib/action_controller/metal/implicit_render.rb +67 -0
- data/lib/action_controller/metal/instrumentation.rb +120 -0
- data/lib/action_controller/metal/live.rb +398 -0
- data/lib/action_controller/metal/logging.rb +22 -0
- data/lib/action_controller/metal/mime_responds.rb +337 -0
- data/lib/action_controller/metal/parameter_encoding.rb +84 -0
- data/lib/action_controller/metal/params_wrapper.rb +312 -0
- data/lib/action_controller/metal/permissions_policy.rb +38 -0
- data/lib/action_controller/metal/rate_limiting.rb +62 -0
- data/lib/action_controller/metal/redirecting.rb +251 -0
- data/lib/action_controller/metal/renderers.rb +181 -0
- data/lib/action_controller/metal/rendering.rb +260 -0
- data/lib/action_controller/metal/request_forgery_protection.rb +667 -0
- data/lib/action_controller/metal/rescue.rb +33 -0
- data/lib/action_controller/metal/streaming.rb +183 -0
- data/lib/action_controller/metal/strong_parameters.rb +1546 -0
- data/lib/action_controller/metal/testing.rb +25 -0
- data/lib/action_controller/metal/url_for.rb +65 -0
- data/lib/action_controller/metal.rb +339 -0
- data/lib/action_controller/railtie.rb +149 -0
- data/lib/action_controller/railties/helpers.rb +26 -0
- data/lib/action_controller/renderer.rb +161 -0
- data/lib/action_controller/template_assertions.rb +13 -0
- data/lib/action_controller/test_case.rb +691 -0
- data/lib/action_controller.rb +80 -0
- data/lib/action_dispatch/constants.rb +34 -0
- data/lib/action_dispatch/deprecator.rb +9 -0
- data/lib/action_dispatch/http/cache.rb +249 -0
- data/lib/action_dispatch/http/content_disposition.rb +47 -0
- data/lib/action_dispatch/http/content_security_policy.rb +365 -0
- data/lib/action_dispatch/http/filter_parameters.rb +80 -0
- data/lib/action_dispatch/http/filter_redirect.rb +50 -0
- data/lib/action_dispatch/http/headers.rb +134 -0
- data/lib/action_dispatch/http/mime_negotiation.rb +187 -0
- data/lib/action_dispatch/http/mime_type.rb +389 -0
- data/lib/action_dispatch/http/mime_types.rb +54 -0
- data/lib/action_dispatch/http/parameters.rb +119 -0
- data/lib/action_dispatch/http/permissions_policy.rb +189 -0
- data/lib/action_dispatch/http/rack_cache.rb +67 -0
- data/lib/action_dispatch/http/request.rb +498 -0
- data/lib/action_dispatch/http/response.rb +556 -0
- data/lib/action_dispatch/http/upload.rb +107 -0
- data/lib/action_dispatch/http/url.rb +344 -0
- data/lib/action_dispatch/journey/formatter.rb +226 -0
- data/lib/action_dispatch/journey/gtg/builder.rb +149 -0
- data/lib/action_dispatch/journey/gtg/simulator.rb +50 -0
- data/lib/action_dispatch/journey/gtg/transition_table.rb +217 -0
- data/lib/action_dispatch/journey/nfa/dot.rb +27 -0
- data/lib/action_dispatch/journey/nodes/node.rb +208 -0
- data/lib/action_dispatch/journey/parser.rb +103 -0
- data/lib/action_dispatch/journey/path/pattern.rb +209 -0
- data/lib/action_dispatch/journey/route.rb +189 -0
- data/lib/action_dispatch/journey/router/utils.rb +105 -0
- data/lib/action_dispatch/journey/router.rb +151 -0
- data/lib/action_dispatch/journey/routes.rb +82 -0
- data/lib/action_dispatch/journey/scanner.rb +70 -0
- data/lib/action_dispatch/journey/visitors.rb +267 -0
- data/lib/action_dispatch/journey/visualizer/fsm.css +30 -0
- data/lib/action_dispatch/journey/visualizer/fsm.js +159 -0
- data/lib/action_dispatch/journey/visualizer/index.html.erb +52 -0
- data/lib/action_dispatch/journey.rb +7 -0
- data/lib/action_dispatch/log_subscriber.rb +25 -0
- data/lib/action_dispatch/middleware/actionable_exceptions.rb +46 -0
- data/lib/action_dispatch/middleware/assume_ssl.rb +27 -0
- data/lib/action_dispatch/middleware/callbacks.rb +38 -0
- data/lib/action_dispatch/middleware/cookies.rb +719 -0
- data/lib/action_dispatch/middleware/debug_exceptions.rb +206 -0
- data/lib/action_dispatch/middleware/debug_locks.rb +129 -0
- data/lib/action_dispatch/middleware/debug_view.rb +73 -0
- data/lib/action_dispatch/middleware/exception_wrapper.rb +350 -0
- data/lib/action_dispatch/middleware/executor.rb +32 -0
- data/lib/action_dispatch/middleware/flash.rb +318 -0
- data/lib/action_dispatch/middleware/host_authorization.rb +171 -0
- data/lib/action_dispatch/middleware/public_exceptions.rb +64 -0
- data/lib/action_dispatch/middleware/reloader.rb +16 -0
- data/lib/action_dispatch/middleware/remote_ip.rb +199 -0
- data/lib/action_dispatch/middleware/request_id.rb +50 -0
- data/lib/action_dispatch/middleware/server_timing.rb +78 -0
- data/lib/action_dispatch/middleware/session/abstract_store.rb +112 -0
- data/lib/action_dispatch/middleware/session/cache_store.rb +66 -0
- data/lib/action_dispatch/middleware/session/cookie_store.rb +129 -0
- data/lib/action_dispatch/middleware/session/mem_cache_store.rb +34 -0
- data/lib/action_dispatch/middleware/show_exceptions.rb +88 -0
- data/lib/action_dispatch/middleware/ssl.rb +180 -0
- data/lib/action_dispatch/middleware/stack.rb +194 -0
- data/lib/action_dispatch/middleware/static.rb +192 -0
- data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
- data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
- data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
- data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +17 -0
- data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +23 -0
- data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +36 -0
- data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
- data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +62 -0
- data/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb +9 -0
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +12 -0
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +9 -0
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +35 -0
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +9 -0
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +24 -0
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +16 -0
- data/lib/action_dispatch/middleware/templates/rescues/layout.erb +284 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +23 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +11 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb +3 -0
- data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +32 -0
- data/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb +11 -0
- data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +20 -0
- data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +7 -0
- data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +6 -0
- data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +3 -0
- data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +19 -0
- data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +232 -0
- data/lib/action_dispatch/railtie.rb +77 -0
- data/lib/action_dispatch/request/session.rb +283 -0
- data/lib/action_dispatch/request/utils.rb +109 -0
- data/lib/action_dispatch/routing/endpoint.rb +19 -0
- data/lib/action_dispatch/routing/inspector.rb +323 -0
- data/lib/action_dispatch/routing/mapper.rb +2372 -0
- data/lib/action_dispatch/routing/polymorphic_routes.rb +363 -0
- data/lib/action_dispatch/routing/redirection.rb +218 -0
- data/lib/action_dispatch/routing/route_set.rb +958 -0
- data/lib/action_dispatch/routing/routes_proxy.rb +66 -0
- data/lib/action_dispatch/routing/url_for.rb +244 -0
- data/lib/action_dispatch/routing.rb +262 -0
- data/lib/action_dispatch/system_test_case.rb +206 -0
- data/lib/action_dispatch/system_testing/browser.rb +75 -0
- data/lib/action_dispatch/system_testing/driver.rb +85 -0
- data/lib/action_dispatch/system_testing/server.rb +33 -0
- data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +164 -0
- data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +23 -0
- data/lib/action_dispatch/testing/assertion_response.rb +48 -0
- data/lib/action_dispatch/testing/assertions/response.rb +114 -0
- data/lib/action_dispatch/testing/assertions/routing.rb +343 -0
- data/lib/action_dispatch/testing/assertions.rb +25 -0
- data/lib/action_dispatch/testing/integration.rb +694 -0
- data/lib/action_dispatch/testing/request_encoder.rb +60 -0
- data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
- data/lib/action_dispatch/testing/test_process.rb +57 -0
- data/lib/action_dispatch/testing/test_request.rb +73 -0
- data/lib/action_dispatch/testing/test_response.rb +58 -0
- data/lib/action_dispatch.rb +147 -0
- data/lib/action_pack/gem_version.rb +19 -0
- data/lib/action_pack/version.rb +12 -0
- data/lib/action_pack.rb +27 -0
- metadata +375 -0
@@ -0,0 +1,565 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
require "base64"
|
6
|
+
require "active_support/security_utils"
|
7
|
+
require "active_support/core_ext/array/access"
|
8
|
+
|
9
|
+
module ActionController
|
10
|
+
# HTTP Basic, Digest, and Token authentication.
|
11
|
+
module HttpAuthentication
|
12
|
+
# # HTTP Basic authentication
|
13
|
+
#
|
14
|
+
# ### Simple Basic example
|
15
|
+
#
|
16
|
+
# class PostsController < ApplicationController
|
17
|
+
# http_basic_authenticate_with name: "dhh", password: "secret", except: :index
|
18
|
+
#
|
19
|
+
# def index
|
20
|
+
# render plain: "Everyone can see me!"
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# def edit
|
24
|
+
# render plain: "I'm only accessible if you know the password"
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# ### Advanced Basic example
|
29
|
+
#
|
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:
|
33
|
+
#
|
34
|
+
# class ApplicationController < ActionController::Base
|
35
|
+
# before_action :set_account, :authenticate
|
36
|
+
#
|
37
|
+
# private
|
38
|
+
# def set_account
|
39
|
+
# @account = Account.find_by(url_name: request.subdomains.first)
|
40
|
+
# end
|
41
|
+
#
|
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
|
+
# else
|
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
|
56
|
+
# end
|
57
|
+
# end
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# In your integration tests, you can do something like this:
|
61
|
+
#
|
62
|
+
# def test_access_granted_from_xml
|
63
|
+
# authorization = ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password)
|
64
|
+
#
|
65
|
+
# get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization }
|
66
|
+
#
|
67
|
+
# assert_equal 200, status
|
68
|
+
# end
|
69
|
+
module Basic
|
70
|
+
extend self
|
71
|
+
|
72
|
+
module ControllerMethods
|
73
|
+
extend ActiveSupport::Concern
|
74
|
+
|
75
|
+
module ClassMethods
|
76
|
+
# Enables HTTP Basic authentication.
|
77
|
+
#
|
78
|
+
# See ActionController::HttpAuthentication::Basic for example usage.
|
79
|
+
def http_basic_authenticate_with(name:, password:, realm: nil, **options)
|
80
|
+
raise ArgumentError, "Expected name: to be a String, got #{name.class}" unless name.is_a?(String)
|
81
|
+
raise ArgumentError, "Expected password: to be a String, got #{password.class}" unless password.is_a?(String)
|
82
|
+
before_action(options) { http_basic_authenticate_or_request_with name: name, password: password, realm: realm }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def http_basic_authenticate_or_request_with(name:, password:, realm: nil, message: nil)
|
87
|
+
authenticate_or_request_with_http_basic(realm, message) do |given_name, given_password|
|
88
|
+
# This comparison uses & so that it doesn't short circuit and uses
|
89
|
+
# `secure_compare` so that length information isn't leaked.
|
90
|
+
ActiveSupport::SecurityUtils.secure_compare(given_name.to_s, name) &
|
91
|
+
ActiveSupport::SecurityUtils.secure_compare(given_password.to_s, password)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def authenticate_or_request_with_http_basic(realm = nil, message = nil, &login_procedure)
|
96
|
+
authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm || "Application", message)
|
97
|
+
end
|
98
|
+
|
99
|
+
def authenticate_with_http_basic(&login_procedure)
|
100
|
+
HttpAuthentication::Basic.authenticate(request, &login_procedure)
|
101
|
+
end
|
102
|
+
|
103
|
+
def request_http_basic_authentication(realm = "Application", message = nil)
|
104
|
+
HttpAuthentication::Basic.authentication_request(self, realm, message)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def authenticate(request, &login_procedure)
|
109
|
+
if has_basic_credentials?(request)
|
110
|
+
login_procedure.call(*user_name_and_password(request))
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def has_basic_credentials?(request)
|
115
|
+
request.authorization.present? && (auth_scheme(request).downcase == "basic")
|
116
|
+
end
|
117
|
+
|
118
|
+
def user_name_and_password(request)
|
119
|
+
decode_credentials(request).split(":", 2)
|
120
|
+
end
|
121
|
+
|
122
|
+
def decode_credentials(request)
|
123
|
+
::Base64.decode64(auth_param(request) || "")
|
124
|
+
end
|
125
|
+
|
126
|
+
def auth_scheme(request)
|
127
|
+
request.authorization.to_s.split(" ", 2).first
|
128
|
+
end
|
129
|
+
|
130
|
+
def auth_param(request)
|
131
|
+
request.authorization.to_s.split(" ", 2).second
|
132
|
+
end
|
133
|
+
|
134
|
+
def encode_credentials(user_name, password)
|
135
|
+
"Basic #{::Base64.strict_encode64("#{user_name}:#{password}")}"
|
136
|
+
end
|
137
|
+
|
138
|
+
def authentication_request(controller, realm, message)
|
139
|
+
message ||= "HTTP Basic: Access denied.\n"
|
140
|
+
controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.tr('"', "")}")
|
141
|
+
controller.status = 401
|
142
|
+
controller.response_body = message
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# # HTTP Digest authentication
|
147
|
+
#
|
148
|
+
# ### Simple Digest example
|
149
|
+
#
|
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
|
155
|
+
#
|
156
|
+
# before_action :authenticate, except: [:index]
|
157
|
+
#
|
158
|
+
# def index
|
159
|
+
# render plain: "Everyone can see me!"
|
160
|
+
# end
|
161
|
+
#
|
162
|
+
# def edit
|
163
|
+
# render plain: "I'm only accessible if you know the password"
|
164
|
+
# end
|
165
|
+
#
|
166
|
+
# private
|
167
|
+
# def authenticate
|
168
|
+
# authenticate_or_request_with_http_digest(REALM) do |username|
|
169
|
+
# USERS[username]
|
170
|
+
# end
|
171
|
+
# end
|
172
|
+
# end
|
173
|
+
#
|
174
|
+
# ### Notes
|
175
|
+
#
|
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.
|
180
|
+
#
|
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.
|
185
|
+
#
|
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.
|
189
|
+
module Digest
|
190
|
+
extend self
|
191
|
+
|
192
|
+
module ControllerMethods
|
193
|
+
# Authenticate using an HTTP Digest, or otherwise render an HTTP header
|
194
|
+
# requesting the client to send a Digest.
|
195
|
+
#
|
196
|
+
# See ActionController::HttpAuthentication::Digest for example usage.
|
197
|
+
def authenticate_or_request_with_http_digest(realm = "Application", message = nil, &password_procedure)
|
198
|
+
authenticate_with_http_digest(realm, &password_procedure) || request_http_digest_authentication(realm, message)
|
199
|
+
end
|
200
|
+
|
201
|
+
# Authenticate using an HTTP Digest. Returns true if authentication is
|
202
|
+
# successful, false otherwise.
|
203
|
+
def authenticate_with_http_digest(realm = "Application", &password_procedure)
|
204
|
+
HttpAuthentication::Digest.authenticate(request, realm, &password_procedure)
|
205
|
+
end
|
206
|
+
|
207
|
+
# Render an HTTP header requesting the client to send a Digest for
|
208
|
+
# authentication.
|
209
|
+
def request_http_digest_authentication(realm = "Application", message = nil)
|
210
|
+
HttpAuthentication::Digest.authentication_request(self, realm, message)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# Returns true on a valid response, false otherwise.
|
215
|
+
def authenticate(request, realm, &password_procedure)
|
216
|
+
request.authorization && validate_digest_response(request, realm, &password_procedure)
|
217
|
+
end
|
218
|
+
|
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.
|
222
|
+
def validate_digest_response(request, realm, &password_procedure)
|
223
|
+
secret_key = secret_token(request)
|
224
|
+
credentials = decode_credentials_header(request)
|
225
|
+
valid_nonce = validate_nonce(secret_key, request, credentials[:nonce])
|
226
|
+
|
227
|
+
if valid_nonce && realm == credentials[:realm] && opaque(secret_key) == credentials[:opaque]
|
228
|
+
password = password_procedure.call(credentials[:username])
|
229
|
+
return false unless password
|
230
|
+
|
231
|
+
method = request.get_header("rack.methodoverride.original_method") || request.get_header("REQUEST_METHOD")
|
232
|
+
uri = credentials[:uri]
|
233
|
+
|
234
|
+
[true, false].any? do |trailing_question_mark|
|
235
|
+
[true, false].any? do |password_is_ha1|
|
236
|
+
_uri = trailing_question_mark ? uri + "?" : uri
|
237
|
+
expected = expected_response(method, _uri, credentials, password, password_is_ha1)
|
238
|
+
expected == credentials[:response]
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
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.
|
248
|
+
def expected_response(http_method, uri, credentials, password, password_is_ha1 = true)
|
249
|
+
ha1 = password_is_ha1 ? password : ha1(credentials, password)
|
250
|
+
ha2 = OpenSSL::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(":"))
|
251
|
+
OpenSSL::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(":"))
|
252
|
+
end
|
253
|
+
|
254
|
+
def ha1(credentials, password)
|
255
|
+
OpenSSL::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(":"))
|
256
|
+
end
|
257
|
+
|
258
|
+
def encode_credentials(http_method, credentials, password, password_is_ha1)
|
259
|
+
credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password, password_is_ha1)
|
260
|
+
"Digest " + credentials.sort_by { |x| x[0].to_s }.map { |v| "#{v[0]}='#{v[1]}'" }.join(", ")
|
261
|
+
end
|
262
|
+
|
263
|
+
def decode_credentials_header(request)
|
264
|
+
decode_credentials(request.authorization)
|
265
|
+
end
|
266
|
+
|
267
|
+
def decode_credentials(header)
|
268
|
+
ActiveSupport::HashWithIndifferentAccess[header.to_s.gsub(/^Digest\s+/, "").split(",").map do |pair|
|
269
|
+
key, value = pair.split("=", 2)
|
270
|
+
[key.strip, value.to_s.gsub(/^"|"$/, "").delete("'")]
|
271
|
+
end]
|
272
|
+
end
|
273
|
+
|
274
|
+
def authentication_header(controller, realm)
|
275
|
+
secret_key = secret_token(controller.request)
|
276
|
+
nonce = self.nonce(secret_key)
|
277
|
+
opaque = opaque(secret_key)
|
278
|
+
controller.headers["WWW-Authenticate"] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce}", opaque="#{opaque}")
|
279
|
+
end
|
280
|
+
|
281
|
+
def authentication_request(controller, realm, message = nil)
|
282
|
+
message ||= "HTTP Digest: Access denied.\n"
|
283
|
+
authentication_header(controller, realm)
|
284
|
+
controller.status = 401
|
285
|
+
controller.response_body = message
|
286
|
+
end
|
287
|
+
|
288
|
+
def secret_token(request)
|
289
|
+
key_generator = request.key_generator
|
290
|
+
http_auth_salt = request.http_auth_salt
|
291
|
+
key_generator.generate_key(http_auth_salt)
|
292
|
+
end
|
293
|
+
|
294
|
+
# Uses an MD5 digest based on time to generate a value to be used only once.
|
295
|
+
#
|
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.
|
300
|
+
#
|
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
|
304
|
+
#
|
305
|
+
# time-stamp H(time-stamp ":" ETag ":" private-key)
|
306
|
+
#
|
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.)
|
320
|
+
#
|
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.
|
326
|
+
#
|
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.
|
330
|
+
def nonce(secret_key, time = Time.now)
|
331
|
+
t = time.to_i
|
332
|
+
hashed = [t, secret_key]
|
333
|
+
digest = OpenSSL::Digest::MD5.hexdigest(hashed.join(":"))
|
334
|
+
::Base64.strict_encode64("#{t}:#{digest}")
|
335
|
+
end
|
336
|
+
|
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.
|
341
|
+
def validate_nonce(secret_key, request, value, seconds_to_timeout = 5 * 60)
|
342
|
+
return false if value.nil?
|
343
|
+
t = ::Base64.decode64(value).split(":").first.to_i
|
344
|
+
nonce(secret_key, t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout
|
345
|
+
end
|
346
|
+
|
347
|
+
# Opaque based on digest of secret key
|
348
|
+
def opaque(secret_key)
|
349
|
+
OpenSSL::Digest::MD5.hexdigest(secret_key)
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
# # HTTP Token authentication
|
354
|
+
#
|
355
|
+
# ### Simple Token example
|
356
|
+
#
|
357
|
+
# class PostsController < ApplicationController
|
358
|
+
# TOKEN = "secret"
|
359
|
+
#
|
360
|
+
# before_action :authenticate, except: [ :index ]
|
361
|
+
#
|
362
|
+
# def index
|
363
|
+
# render plain: "Everyone can see me!"
|
364
|
+
# end
|
365
|
+
#
|
366
|
+
# def edit
|
367
|
+
# render plain: "I'm only accessible if you know the password"
|
368
|
+
# end
|
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
|
379
|
+
#
|
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:
|
383
|
+
#
|
384
|
+
# class ApplicationController < ActionController::Base
|
385
|
+
# before_action :set_account, :authenticate
|
386
|
+
#
|
387
|
+
# private
|
388
|
+
# def set_account
|
389
|
+
# @account = Account.find_by(url_name: request.subdomains.first)
|
390
|
+
# end
|
391
|
+
#
|
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
|
400
|
+
# else
|
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
|
406
|
+
# end
|
407
|
+
# end
|
408
|
+
# end
|
409
|
+
#
|
410
|
+
# In your integration tests, you can do something like this:
|
411
|
+
#
|
412
|
+
# def test_access_granted_from_xml
|
413
|
+
# authorization = ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token)
|
414
|
+
#
|
415
|
+
# get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization }
|
416
|
+
#
|
417
|
+
# assert_equal 200, status
|
418
|
+
# end
|
419
|
+
#
|
420
|
+
# On shared hosts, Apache sometimes doesn't pass authentication headers to FCGI
|
421
|
+
# instances. If your environment matches this description and you cannot
|
422
|
+
# authenticate, try this rule in your Apache setup:
|
423
|
+
#
|
424
|
+
# RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]
|
425
|
+
module Token
|
426
|
+
TOKEN_KEY = "token="
|
427
|
+
TOKEN_REGEX = /^(Token|Bearer)\s+/
|
428
|
+
AUTHN_PAIR_DELIMITERS = /(?:,|;|\t)/
|
429
|
+
extend self
|
430
|
+
|
431
|
+
module ControllerMethods
|
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.
|
436
|
+
#
|
437
|
+
# See ActionController::HttpAuthentication::Token for example usage.
|
438
|
+
def authenticate_or_request_with_http_token(realm = "Application", message = nil, &login_procedure)
|
439
|
+
authenticate_with_http_token(&login_procedure) || request_http_token_authentication(realm, message)
|
440
|
+
end
|
441
|
+
|
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.
|
446
|
+
def authenticate_with_http_token(&login_procedure)
|
447
|
+
Token.authenticate(self, &login_procedure)
|
448
|
+
end
|
449
|
+
|
450
|
+
# Render an HTTP header requesting the client to send a Bearer token for
|
451
|
+
# authentication.
|
452
|
+
def request_http_token_authentication(realm = "Application", message = nil)
|
453
|
+
Token.authentication_request(self, realm, message)
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
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.
|
462
|
+
#
|
463
|
+
# #### Parameters
|
464
|
+
#
|
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| ... }
|
470
|
+
#
|
471
|
+
#
|
472
|
+
def authenticate(controller, &login_procedure)
|
473
|
+
token, options = token_and_options(controller.request)
|
474
|
+
unless token.blank?
|
475
|
+
login_procedure.call(token, options)
|
476
|
+
end
|
477
|
+
end
|
478
|
+
|
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"
|
484
|
+
#
|
485
|
+
# Then the returned token is `"abc"`, and the options are `{nonce: "def"}`.
|
486
|
+
#
|
487
|
+
# Returns an `Array` of `[String, Hash]` if a token is present. Returns `nil` if
|
488
|
+
# no token is found.
|
489
|
+
#
|
490
|
+
# #### Parameters
|
491
|
+
#
|
492
|
+
# * `request` - ActionDispatch::Request instance with the current headers.
|
493
|
+
#
|
494
|
+
def token_and_options(request)
|
495
|
+
authorization_request = request.authorization.to_s
|
496
|
+
if authorization_request[TOKEN_REGEX]
|
497
|
+
params = token_params_from authorization_request
|
498
|
+
[params.shift[1], Hash[params].with_indifferent_access]
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
def token_params_from(auth)
|
503
|
+
rewrite_param_values params_array_from raw_params auth
|
504
|
+
end
|
505
|
+
|
506
|
+
# Takes `raw_params` and turns it into an array of parameters.
|
507
|
+
def params_array_from(raw_params)
|
508
|
+
raw_params.map { |param| param.split %r/=(.+)?/ }
|
509
|
+
end
|
510
|
+
|
511
|
+
# This removes the `"` characters wrapping the value.
|
512
|
+
def rewrite_param_values(array_params)
|
513
|
+
array_params.each { |param| (param[1] || +"").gsub! %r/^"|"$/, "" }
|
514
|
+
end
|
515
|
+
|
516
|
+
WHITESPACED_AUTHN_PAIR_DELIMITERS = /\s*#{AUTHN_PAIR_DELIMITERS}\s*/
|
517
|
+
private_constant :WHITESPACED_AUTHN_PAIR_DELIMITERS
|
518
|
+
|
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`.
|
522
|
+
def raw_params(auth)
|
523
|
+
_raw_params = auth.sub(TOKEN_REGEX, "").split(WHITESPACED_AUTHN_PAIR_DELIMITERS)
|
524
|
+
_raw_params.reject!(&:empty?)
|
525
|
+
|
526
|
+
if !_raw_params.first&.start_with?(TOKEN_KEY)
|
527
|
+
_raw_params[0] = "#{TOKEN_KEY}#{_raw_params.first}"
|
528
|
+
end
|
529
|
+
|
530
|
+
_raw_params
|
531
|
+
end
|
532
|
+
|
533
|
+
# Encodes the given token and options into an Authorization header value.
|
534
|
+
#
|
535
|
+
# Returns String.
|
536
|
+
#
|
537
|
+
# #### Parameters
|
538
|
+
#
|
539
|
+
# * `token` - String token.
|
540
|
+
# * `options` - Optional Hash of the options.
|
541
|
+
#
|
542
|
+
def encode_credentials(token, options = {})
|
543
|
+
values = ["#{TOKEN_KEY}#{token.to_s.inspect}"] + options.map do |key, value|
|
544
|
+
"#{key}=#{value.to_s.inspect}"
|
545
|
+
end
|
546
|
+
"Token #{values * ", "}"
|
547
|
+
end
|
548
|
+
|
549
|
+
# Sets a WWW-Authenticate header to let the client know a token is desired.
|
550
|
+
#
|
551
|
+
# Returns nothing.
|
552
|
+
#
|
553
|
+
# #### Parameters
|
554
|
+
#
|
555
|
+
# * `controller` - ActionController::Base instance for the outgoing response.
|
556
|
+
# * `realm` - String realm to use in the header.
|
557
|
+
#
|
558
|
+
def authentication_request(controller, realm, message = nil)
|
559
|
+
message ||= "HTTP Token: Access denied.\n"
|
560
|
+
controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"', "")}")
|
561
|
+
controller.__send__ :render, plain: message, status: :unauthorized
|
562
|
+
end
|
563
|
+
end
|
564
|
+
end
|
565
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
module ActionController
|
6
|
+
# # Action Controller Implicit Render
|
7
|
+
#
|
8
|
+
# Handles implicit rendering for a controller action that does not explicitly
|
9
|
+
# respond with `render`, `respond_to`, `redirect`, or `head`.
|
10
|
+
#
|
11
|
+
# For API controllers, the implicit response is always `204 No Content`.
|
12
|
+
#
|
13
|
+
# For all other controllers, we use these heuristics to decide whether to render
|
14
|
+
# a template, raise an error for a missing template, or respond with `204 No
|
15
|
+
# Content`:
|
16
|
+
#
|
17
|
+
# First, if we DO find a template, it's rendered. Template lookup accounts for
|
18
|
+
# the action name, locales, format, variant, template handlers, and more (see
|
19
|
+
# `render` for details).
|
20
|
+
#
|
21
|
+
# Second, if we DON'T find a template but the controller action does have
|
22
|
+
# templates for other formats, variants, etc., then we trust that you meant to
|
23
|
+
# provide a template for this response, too, and we raise
|
24
|
+
# ActionController::UnknownFormat with an explanation.
|
25
|
+
#
|
26
|
+
# Third, if we DON'T find a template AND the request is a page load in a web
|
27
|
+
# browser (technically, a non-XHR GET request for an HTML response) where you
|
28
|
+
# reasonably expect to have rendered a template, then we raise
|
29
|
+
# ActionController::MissingExactTemplate with an explanation.
|
30
|
+
#
|
31
|
+
# Finally, if we DON'T find a template AND the request isn't a browser page
|
32
|
+
# load, then we implicitly respond with `204 No Content`.
|
33
|
+
module ImplicitRender
|
34
|
+
# :stopdoc:
|
35
|
+
include BasicImplicitRender
|
36
|
+
|
37
|
+
def default_render
|
38
|
+
if template_exists?(action_name.to_s, _prefixes, variants: request.variant)
|
39
|
+
render
|
40
|
+
elsif any_templates?(action_name.to_s, _prefixes)
|
41
|
+
message = "#{self.class.name}\##{action_name} is missing a template " \
|
42
|
+
"for this request format and variant.\n" \
|
43
|
+
"\nrequest.formats: #{request.formats.map(&:to_s).inspect}" \
|
44
|
+
"\nrequest.variant: #{request.variant.inspect}"
|
45
|
+
|
46
|
+
raise ActionController::UnknownFormat, message
|
47
|
+
elsif interactive_browser_request?
|
48
|
+
message = "#{self.class.name}\##{action_name} is missing a template for request formats: #{request.formats.map(&:to_s).join(',')}"
|
49
|
+
raise ActionController::MissingExactTemplate.new(message, self.class, action_name)
|
50
|
+
else
|
51
|
+
logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger
|
52
|
+
super
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def method_for_action(action_name)
|
57
|
+
super || if template_exists?(action_name.to_s, _prefixes)
|
58
|
+
"default_render"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
def interactive_browser_request?
|
64
|
+
request.get? && request.format == Mime[:html] && !request.xhr?
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|