Spec-Zone.ru › Ruby on Rails 5.2

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

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

authenticate(request, realm, &password_procedure) Показать исходный код
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 197
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 255
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 262
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 248
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 244
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 239
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 229
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 вместо пароля в текстовом формате.

ha1(credentials, password) Показать исходный код
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 235
def ha1(credentials, password)
  ::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 307
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 digest, основанный на времени, для генерации значения, используемого только один раз.

Заданная сервером строка данных, которая должна генерироваться уникальным образом каждый раз, когда создается ответ 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 или ранее используемый digest, чтобы защитить от атаки с повтором. Или же реализация может выбрать использование одноразовых nonce или digest для запросов POST, PUT или PATCH и временной метки для запросов GET. Для получения более подробной информации о связанных проблемах см. раздел 4 этого документа.

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

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

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

secret_token(request) Показать исходный код
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 269
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 204
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. Если это не сработает, попробуйте его как пароль в текстовом формате.

validate_nonce(secret_key, request, value, seconds_to_timeout = 5 * 60) Показать исходный код
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 319
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