модуль 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 для корректного хеширования и проверки учетных данных. Возврат nil приведет к сбою аутентификации.
Хранение хэша ha1: MD5(имя_пользователя:область:пароль) предпочтительнее хранения простого пароля. Если файл паролей или база данных взломаны, злоумышленник сможет использовать хэш ha1 для аутентификации как пользователь в этом realm, но не получит пароль пользователя для использования на других сайтах.
В редких случаях веб-серверы или прокси-серверы перед передачей запроса вашей приложению удаляют заголовки авторизации. Вы можете отладить такую ситуацию, выведя в лог все переменные окружения и проверив заголовок HTTP_AUTHORIZATION и другие.
Публичные методы экземпляра
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 195 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 253
def authentication_header(controller, realm)
secret_key = secret_token(controller.request)
nonce = self.nonce(secret_key)
opaque = opaque(secret_key)
controller.headers["WWW-Authenticate"] = %Q(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce}", opaque="#{opaque}")
end # File actionpack/lib/action_controller/metal/http_authentication.rb, line 260 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 246
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 242 def decode_credentials_header(request) decode_credentials(request.authorization) end
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 237
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 227
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 вместо простого пароля в текстовом формате.
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 233
def ha1(credentials, password)
::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(":"))
end # File actionpack/lib/action_controller/metal/http_authentication.rb, line 305
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 хэш, основанный на времени, для генерации одноразового значения.
Указывается строка данных, предоставляемая сервером, которая должна генерироваться уникальным образом каждый раз, когда генерируется ответ 401. Рекомендуется использовать данные в формате base64 или шестнадцатеричном формате. В частности, поскольку строка передаётся в строках заголовка как строка в кавычках, символ двойной кавычки недопустим.
Содержание nonce зависит от реализации. Качество реализации зависит от правильного выбора. Например, nonce может быть составлен как base64-кодирование
time-stamp H(time-stamp ":" ETag ":" private-key)
где временная метка — сгенерированное сервером время или другое не повторяющееся значение, ETag — значение заголовка HTTP ETag, связанное с запрашиваемым объектом, а частный ключ — данные, известные только серверу. При таком виде nonce сервер пересчитает хэш-часть после получения заголовка аутентификации клиента и отклонит запрос, если он не совпадает с nonce из этого заголовка или если значение временной метки недостаточно свежее. Таким образом сервер может ограничить срок действия nonce. Включение ETag предотвращает повторный запрос для обновлённой версии ресурса. (Примечание: включение IP-адреса клиента в nonce, по-видимому, даёт серверу возможность ограничить повторное использование nonce только для клиента, который его первоначально получил. Однако это нарушит работу ферм прокси, где запросы от одного пользователя часто проходят через разные прокси в ферме. Кроме того, подмена IP-адреса не так уж сложна.)
Реализация может выбрать отказ от ранее использованного nonce или хэша, чтобы защититься от атаки с повтором запроса. Или же реализация может выбрать использование одноразовых nonce или хэшей для запросов POST, PUT или PATCH и временную метку для запросов GET. Подробнее об этом можно прочитать в разделе 4 этого документа.
Nonce невидим для клиента. Составлен из Time и хэша Time с секретным ключом из секрета сессии Rails, сгенерированного при создании проекта. Гарантирует, что время не может быть изменено клиентом.
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 324 def opaque(secret_key) ::Digest::MD5.hexdigest(secret_key) end
Непрозрачное значение на основе хэша секретного ключа
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 267 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 202
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 317
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–2018 David Heinemeier Hansson
Licensed under the MIT License.