модуль ActionController::HttpAuthentication::Digest
Аутентификация HTTP Digest.
Пример Digest
require "openssl"
class PostsController < ApplicationController
REALM = "SuperSecret"
USERS = {"dhh" => "secret", #plain text password
"dap" => OpenSSL::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 200 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 258
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 265 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 251
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 247 def decode_credentials_header(request) decode_credentials(request.authorization) end
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 242
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 232
def expected_response(http_method, uri, credentials, password, password_is_ha1 = true)
ha1 = password_is_ha1 ? password : ha1(credentials, password)
ha2 = OpenSSL::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(":"))
OpenSSL::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 238
def ha1(credentials, password)
OpenSSL::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(":"))
end # File actionpack/lib/action_controller/metal/http_authentication.rb, line 310
def nonce(secret_key, time = Time.now)
t = time.to_i
hashed = [t, secret_key]
digest = OpenSSL::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)
где time-stamp — сгенерированное сервером время или другое неповторяющееся значение, ETag — значение заголовка HTTP ETag, связанного с запрашиваемым ресурсом, а private-key — данные, известные только серверу. С nonce такой формы сервер пересчитывает хеш-часть после получения заголовка аутентификации клиента и отклоняет запрос, если он не соответствует nonce из этого заголовка или если значение time-stamp недостаточно свежее. Таким образом, сервер может ограничить срок действия 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 329 def opaque(secret_key) OpenSSL::Digest::MD5.hexdigest(secret_key) end
Непрозрачное значение, основанное на хеше секретного ключа
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 272 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 207
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 322
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–2021 David Heinemeier Hansson
Licensed under the MIT License.