Spec-Zone.ru › Ruby on Rails 7.0

модуль 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 для того, чтобы фреймворк мог корректно сгенерировать хеш для проверки учетных данных пользователя. Возврат nil приведет к сбою аутентификации.

Хранение хеша ha1: MD5(имя_пользователя:домен:пароль) предпочтительнее, чем хранение простого пароля. Если файл паролей или база данных скомпрометированы, злоумышленник сможет использовать хеш ha1 для аутентификации как пользователя в этом realm, но не сможет получить пароль пользователя для попыток входа на других сайтах.

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

Публичные методы экземпляров

authenticate(запрос, домен, &процедура_пароля) Показать исходный код
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 200
def authenticate(request, realm, &password_procedure)
  request.authorization && validate_digest_response(request, realm, &password_procedure)
end

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

authentication_header(контроллер, домен) Показать исходный код
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 258
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(контроллер, домен, сообщение = nil) Показать исходный код
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 265
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(заголовок) Показать исходный код
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 251
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(запрос) Показать исходный код
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 247
def decode_credentials_header(request)
  decode_credentials(request.authorization)
end
encode_credentials(http_метод, учетные_данные, пароль, пароль_как_ha1) Показать исходный код
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 242
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_метод, uri, учетные_данные, пароль, пароль_как_ha1 = true) Показать исходный код
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 232
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, а не текстовый пароль.

ha1(учетные_данные, пароль) Показать исходный код
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 238
def ha1(credentials, password)
  OpenSSL::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(":"))
end
nonce(секретный_ключ, время = Time.now) Показать исходный код
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 310
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)

где time-stamp — сгенерированное сервером время или другое неповторяющееся значение, ETag — значение заголовка HTTP ETag, связанного с запрашиваемым ресурсом, а private-key — данные, известные только серверу. С nonce такой формы сервер пересчитывает хеш-часть после получения заголовка аутентификации клиента и отклоняет запрос, если он не соответствует nonce из этого заголовка или если значение time-stamp недостаточно свежее. Таким образом, сервер может ограничить срок действия nonce. Включение ETag предотвращает повторный запрос на обновлённую версию ресурса. (Примечание: включение IP-адреса клиента в nonce, по-видимому, предоставляет серверу возможность ограничить повторное использование nonce для того же клиента, который его изначально получил. Однако это нарушит работу ферм прокси, где запросы от одного пользователя часто проходят через различные прокси в ферме. Кроме того, подмена IP-адреса не так сложна.)

Реализация может выбрать не принимать ранее используемый nonce или хеш, чтобы защитить от атаки методом повтора запроса. Или же реализация может выбрать использовать одноразовые nonce или хеши для запросов POST, PUT или PATCH и отметку времени для запросов GET. Для получения более подробной информации по соответствующим вопросам обратитесь к разделу 4 данного документа.

Nonce непрозрачен для клиента. Составлен из Time и хеша Time с секретным ключом из секрета сессии Rails, сгенерированного при создании проекта. Гарантирует, что время нельзя изменить клиентом.

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

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

secret_token(запрос) Показать исходный код
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 272
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(запрос, домен, &процедура_пароля) Показать исходный код
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 207
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(секретный_ключ, запрос, значение, секунды_до_таймаута = 5 * 60) Показать исходный код
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 322
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