модуль 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 должен возвращать пароль пользователя или хэш digest ha1, чтобы фреймворк мог правильно вычислить хэш для проверки учетных данных пользователя. Возврат nil приведет к отказу в аутентификации.
Хранение хэша ha1: MD5(имя_пользователя:область:пароль), предпочтительнее, чем хранение простого пароля. Если файл или база данных паролей скомпрометированы, злоумышленник сможет использовать хэш ha1 для аутентификации как пользователя в этом realm, но не будет иметь пароль пользователя, чтобы попробовать использовать его на других сайтах.
В редких случаях веб-серверы или прокси-серверы переднего плана удаляют заголовки авторизации перед тем, как они достигнут вашего приложения. Вы можете отладить эту ситуацию, залогировав все переменные окружения и проверив HTTP_AUTHORIZATION и другие.
Методы публичного экземпляра
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 211 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 269
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 276 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 262
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 258 def decode_credentials_header(request) decode_credentials(request.authorization) end
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 253
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 243
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 249
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 321
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)
где временная метка — сгенерированное сервером время или другое не повторяющееся значение, ETag — значение заголовка HTTP ETag, связанного с запрашиваемым объектом, и private-key — данные, известные только серверу. При таком 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 340 def opaque(secret_key) OpenSSL::Digest::MD5.hexdigest(secret_key) end
Непрозрачный, основанный на хэше секретного ключа
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 283 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 218
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 333
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.