Spec-Zone.ru › Ruby on Rails 7.2

модуль 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 и другие.

Методы экземпляра публичного интерфейса

authenticate(request, realm, &password_procedure) Показать исходный код
# 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

Возвращает false при успешном ответе, иначе true.

authentication_header(controller, realm) Показать исходный код
# 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
authentication_request(controller, realm, message = nil) Показать исходный код
# 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
decode_credentials(header) Показать исходный код
# 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
decode_credentials_header(request) Показать исходный код
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 263
def decode_credentials_header(request)
  decode_credentials(request.authorization)
end
encode_credentials(http_method, credentials, password, password_is_ha1) Показать исходный код
# 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
expected_response(http_method, uri, credentials, password, password_is_ha1 = true) Показать исходный код
# 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, а не простого текстового пароля.

ha1(credentials, password) Показать исходный код
# 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
nonce(secret_key, time = Time.now) Показать исходный код
# 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-хэш, основанный на времени, для генерации значения, используемого только один раз.

Указанная сервером строка данных, которая должна генерироваться уникальным образом каждый раз, когда создается ответ 401. Рекомендуется, чтобы эта строка была данными base64 или шестнадцатеричными данными. В частности, поскольку строка передается в строках заголовка как строка в кавычках, символ двойной кавычки не разрешен.

Содержание nonce зависит от реализации. Качество реализации зависит от правильного выбора. Например, nonce может быть составлен как кодирование base 64 от

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 секрета сессии, сгенерированного при создании проекта. Гарантирует, что время не может быть изменено клиентом.

opaque(secret_key) Показать исходный код
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 348
def opaque(secret_key)
  OpenSSL::Digest::MD5.hexdigest(secret_key)
end

Непрозрачный, основанный на хэше секретного ключа

secret_token(request) Показать исходный код
# 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
validate_digest_response(request, realm, &password_procedure) Показать исходный код
# 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. Если это не удастся, попробуйте его как простой текстовый пароль.

validate_nonce(secret_key, request, value, seconds_to_timeout = 5 * 60) Показать исходный код
# 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.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API