модуль ActionController::HttpAuthentication::Digest
Обеспечивает простую реализацию аутентификации HTTP Digest.
Пример простой аутентификации Digest
require "digest/md5"
class PostsController < ApplicationController
REALM = "SuperSecret"
USERS = {"dhh" => "secret", #plain text password
"dap" => Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password
before_action :authenticate, except: [:index]
def index
render plain: "Everyone can see me!"
end
def edit
render plain: "I'm only accessible if you know the password"
end
private
def authenticate
authenticate_or_request_with_http_digest(REALM) do |username|
USERS[username]
end
end
end
Примечания
Блок authenticate_or_request_with_http_digest должен возвращать пароль пользователя или хэш ha1 digest, чтобы фреймворк мог правильно вычислить хэш для проверки учетных данных пользователя. Возвращение nil приведет к отказу в аутентификации.
Хранение хэша ha1: MD5(имя_пользователя:область:пароль), предпочтительнее, чем хранение простого пароля. Если файл паролей или база данных взломаны, злоумышленник сможет использовать хэш ha1 для аутентификации как пользователь в этом realm, но не получит пароль пользователя, чтобы попробовать его использовать на других сайтах.
В редких случаях веб-серверы или прокси-серверы удаляют заголовки авторизации до их достижения вашим приложением. Вы можете отладить эту ситуацию, записав все переменные окружения и проверив HTTP_AUTHORIZATION и другие.
Методы публичного экземпляра
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 199 def authenticate(request, realm, &password_procedure) request.authorization && validate_digest_response(request, realm, &password_procedure) end
Возвращает false при успешной проверке, true в противном случае.
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 257
def authentication_header(controller, realm)
secret_key = secret_token(controller.request)
nonce = self.nonce(secret_key)
opaque = opaque(secret_key)
controller.headers["WWW-Authenticate"] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce}", opaque="#{opaque}")
end # File actionpack/lib/action_controller/metal/http_authentication.rb, line 264 def authentication_request(controller, realm, message = nil) message ||= "HTTP Digest: Access denied.\n" authentication_header(controller, realm) controller.status = 401 controller.response_body = message end
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 250
def decode_credentials(header)
ActiveSupport::HashWithIndifferentAccess[header.to_s.gsub(/^Digest\s+/, "").split(",").map do |pair|
key, value = pair.split("=", 2)
[key.strip, value.to_s.gsub(/^"|"$/, "").delete("'")]
end]
end # File actionpack/lib/action_controller/metal/http_authentication.rb, line 246 def decode_credentials_header(request) decode_credentials(request.authorization) end
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 241
def encode_credentials(http_method, credentials, password, password_is_ha1)
credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password, password_is_ha1)
"Digest " + credentials.sort_by { |x| x[0].to_s }.map { |v| "#{v[0]}='#{v[1]}'" }.join(", ")
end # File actionpack/lib/action_controller/metal/http_authentication.rb, line 231
def expected_response(http_method, uri, credentials, password, password_is_ha1 = true)
ha1 = password_is_ha1 ? password : ha1(credentials, password)
ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(":"))
::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(":"))
end Возвращает ожидаемый ответ для запроса http_method к uri с декодированными credentials и ожидаемым password. Дополнительный параметр password_is_ha1 по умолчанию установлен в true, так как лучшая практика - хранить хэш ha1 digest вместо пароля в текстовом формате.
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 237
def ha1(credentials, password)
::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(":"))
end # File actionpack/lib/action_controller/metal/http_authentication.rb, line 309
def nonce(secret_key, time = Time.now)
t = time.to_i
hashed = [t, secret_key]
digest = ::Digest::MD5.hexdigest(hashed.join(":"))
::Base64.strict_encode64("#{t}:#{digest}")
end Использует MD5 digest, основанный на времени, для генерации значения, используемого только один раз.
Строка данных, указанная сервером, которая должна быть уникальной каждый раз, когда генерируется ответ 401. Рекомендуется, чтобы эта строка была данными в формате base64 или шестнадцатеричных данных. В частности, поскольку строка передается в строках заголовка как строка в кавычках, символ двойной кавычки недопустим.
Содержимое nonce зависит от реализации. Качество реализации зависит от хорошего выбора. Например, nonce может быть построен как кодировка base64 от
time-stamp H(time-stamp ":" ETag ":" private-key)
где time-stamp - сгенерированное сервером время или другое не повторяющееся значение, ETag - значение заголовка HTTP ETag, связанного с запрашиваемым объектом, а private-key - данные, известные только серверу. При использовании nonce такого вида сервер пересчитает хэш-часть после получения заголовка аутентификации клиента и отклонит запрос, если он не совпадает с nonce из этого заголовка или если значение time-stamp недостаточно свежее. Таким образом, сервер может ограничить время действия nonce. Включение ETag предотвращает повторный запрос для обновлённой версии ресурса. (Примечание: включение IP-адреса клиента в nonce, кажется, предоставляет серверу возможность ограничить повторное использование nonce для того же клиента, который его изначально получил. Однако это нарушит работу прокси-ферм, где запросы от одного пользователя часто проходят через разные прокси в ферме. Кроме того, подмена IP-адреса не является сложной задачей.)
Реализация может выбрать не принимать ранее использованный nonce или ранее использованный digest, чтобы защититься от атаки методом повторения. Или реализация может выбрать использовать одноразовые nonce или digest для запросов POST, PUT или PATCH и time-stamp для запросов GET. Более подробную информацию об этих вопросах см. в разделе 4 этого документа.
Nonce невидим для клиента. Составлен из Time, и хэш Time с секретным ключом из секрета сессии Rails, сгенерированного при создании проекта. Гарантирует, что время не может быть изменено клиентом.
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 328 def opaque(secret_key) ::Digest::MD5.hexdigest(secret_key) end
Непрозрачный, основанный на digest секретного ключа
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 271 def secret_token(request) key_generator = request.key_generator http_auth_salt = request.http_auth_salt key_generator.generate_key(http_auth_salt) end
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 206
def validate_digest_response(request, realm, &password_procedure)
secret_key = secret_token(request)
credentials = decode_credentials_header(request)
valid_nonce = validate_nonce(secret_key, request, credentials[:nonce])
if valid_nonce && realm == credentials[:realm] && opaque(secret_key) == credentials[:opaque]
password = password_procedure.call(credentials[:username])
return false unless password
method = request.get_header("rack.methodoverride.original_method") || request.get_header("REQUEST_METHOD")
uri = credentials[:uri]
[true, false].any? do |trailing_question_mark|
[true, false].any? do |password_is_ha1|
_uri = trailing_question_mark ? uri + "?" : uri
expected = expected_response(method, _uri, credentials, password, password_is_ha1)
expected == credentials[:response]
end
end
end
end Возвращает false, если значение ответа учетных данных запроса не соответствует ожидаемому значению. Сначала попробуйте пароль как хэш ha1. Если это не сработает, попробуйте его как обычный пароль.
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 321
def validate_nonce(secret_key, request, value, seconds_to_timeout = 5 * 60)
return false if value.nil?
t = ::Base64.decode64(value).split(":").first.to_i
nonce(secret_key, t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout
end Возможно, потребуется более короткое время ожидания, в зависимости от того, является ли запрос PATCH, PUT или POST, и является ли клиент браузером или веб-сервисом. Он может быть намного короче, если реализован параметр Stale. Это позволит пользователю использовать новый nonce без повторного запроса имени пользователя и пароля.
© 2004–2020 David Heinemeier Hansson
Licensed under the MIT License.