модуль 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 и других заголовков.
Открытые методы экземпляра
Возвращает false при успешном ответе, true в противном случае
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 196 def authenticate(request, realm, &password_procedure) request.authorization && validate_digest_response(request, realm, &password_procedure) end
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 254
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 261 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 247
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 243 def decode_credentials_header(request) decode_credentials(request.authorization) end
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 238
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 Возвращает ожидаемый ответ для запроса http_method к uri с декодированными credentials и ожидаемым password. Дополнительный параметр password_is_ha1 по умолчанию установлен в true, так как лучшей практикой является хранение хэша ha1, а не простого пароля.
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 228
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 # File actionpack/lib/action_controller/metal/http_authentication.rb, line 234
def ha1(credentials, password)
::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(':'))
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 306
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 Непрозрачный, основанный на случайной генерации — но меняющийся с каждым запросом?
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 325 def opaque(secret_key) ::Digest::MD5.hexdigest(secret_key) end
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 268 def secret_token(request) key_generator = request.env["action_dispatch.key_generator"] http_auth_salt = request.env["action_dispatch.http_auth_salt"] key_generator.generate_key(http_auth_salt) end
Возвращает false, если значение ответа аутентификации запроса не совпадает с ожидаемым значением. Сначала попробуйте использовать пароль как хэш ha1. Если это не сработает, попробуйте использовать его как обычный текст.
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 203
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.env['rack.methodoverride.original_method'] || request.env['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 Возможно, потребуется более короткое время ожидания в зависимости от того, является ли запрос PATCH, PUT или POST, и является ли клиент браузером или веб-сервисом. Может быть значительно короче, если реализован директива Stale. Это позволит пользователю использовать новый nonce без повторного запроса имени пользователя и пароля.
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 318
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
© 2004–2016 David Heinemeier Hansson
Licensed under the MIT License.