модуль 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 195 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 253
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 260 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 246
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 242 def decode_credentials_header(request) decode_credentials(request.authorization) end
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 237
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 227
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 233
def ha1(credentials, password)
::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(':'))
end # File actionpack/lib/action_controller/metal/http_authentication.rb, line 305
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-хеш, основанный на времени, для генерации значения, которое будет использоваться только один раз.
Указываемая сервером строка данных, которая должна генерироваться уникальным образом каждый раз, когда генерируется ответ 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 или ранее использованный digest, чтобы защититься от атаки методом повторного использования. Или же реализация может выбрать использование одноразовых nonce или digest для запросов POST, PUT или PATCH и time-stamp для запросов GET. Более подробная информация об этих вопросах представлена в разделе 4 данного документа.
Nonce скрыт от клиента. Составлен из Time и хеша Time с секретным ключом из секрета сессии Rails, сгенерированного при создании проекта. Обеспечивает невозможность изменения времени клиентом.
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 324 def opaque(secret_key) ::Digest::MD5.hexdigest(secret_key) end
Непрозрачный, основанный на хеше секретного ключа
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 267 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 202
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 317
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.