omg-actionpack 8.0.0.alpha1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|