Spec-Zone.ru › Ruby on Rails 5.1

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

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

authenticate(запрос, область, &процедура_пароля) Показать исходный код
# 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 в противном случае

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

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

где временная метка — сгенерированное сервером время или другое не повторяющееся значение, ETag — значение заголовка HTTP ETag, связанное с запрашиваемым объектом, а частный ключ — данные, известные только серверу. При таком виде nonce сервер пересчитает хэш-часть после получения заголовка аутентификации клиента и отклонит запрос, если он не совпадает с nonce из этого заголовка или если значение временной метки недостаточно свежее. Таким образом сервер может ограничить срок действия 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 324
def opaque(secret_key)
  ::Digest::MD5.hexdigest(secret_key)
end

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

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

validate_nonce(секретный_ключ, запрос, значение, секунды_до_таймаута = 5 * 60) Показать исходный код
# 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.

Spec-Zone.ru

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