модуль 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 digest, чтобы фреймворк мог правильно выполнить хеширование для проверки учетных данных пользователя. Возвращение nil приведет к отказу в аутентификации.
Хранение хэша ha1: MD5(имя_пользователя:область:пароль) предпочтительнее хранения простого пароля. Если файл паролей или база данных скомпрометированы, злоумышленник сможет использовать хэш ha1 для аутентификации как пользователя в этом realm, но не получит пароль пользователя, чтобы попытаться использовать его на других сайтах.
В редких случаях веб-серверы или прокси-серверы переднего плана удаляют заголовки авторизации перед передачей запроса в ваше приложение. Вы можете отладить эту ситуацию, ведя журнал всех переменных окружения и проверяя HTTP_AUTHORIZATION и другие.
Публичные методы экземпляров
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 215 def authenticate(request, realm, &password_procedure) request.authorization && validate_digest_response(request, realm, &password_procedure) end
Возвращает true при валидном ответе, иначе false.
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 274
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 281 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 267
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 263 def decode_credentials_header(request) decode_credentials(request.authorization) end
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 258
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 248
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 digest вместо простого пароля.
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 254
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 330
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 digest, основанный на времени, для генерации значения, используемого только один раз.
Указанная сервером строка данных, которая должна генерироваться уникальным образом каждый раз, когда генерируется ответ 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 или ранее используемый digest, чтобы защититься от атаки с повторным использованием. Или реализация может выбрать использование одноразовых nonce или digest для запросов POST, PUT или PATCH и отметку времени для запросов GET. Более подробную информацию об этих вопросах см. в разделе 4 этого документа.
Nonce непрозрачен для клиента. Составлен из Time и хэша Time с секретным ключом из Rails секрета сессии, сгенерированного при создании проекта. Обеспечивает, что время не может быть изменено клиентом.
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 348 def opaque(secret_key) OpenSSL::Digest::MD5.hexdigest(secret_key) end
Непрозрачный, основанный на хэше секретного ключа
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 288 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 222
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 digest. Если это не удастся, попробуйте его как пароль в обычном тексте.
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 341
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.