модуль 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 197 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 255
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 262 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 248
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 244 def decode_credentials_header(request) decode_credentials(request.authorization) end
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 239
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 229
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 235
def ha1(credentials, password)
::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(":"))
end # File actionpack/lib/action_controller/metal/http_authentication.rb, line 307
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)
где временная метка — это сгенерированное сервером время или другое не повторяющееся значение, 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 326 def opaque(secret_key) ::Digest::MD5.hexdigest(secret_key) end
Непрозрачный, основанный на хеше секретного ключа
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 269 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 204
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 319
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.