Spec-Zone.ru › Ruby on Rails 8.1

module 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(username:realm:password) лучше, чем хранить пароль в открытом виде. Если файл паролей или база данных будут скомпрометированы, злоумышленник сможет использовать хеш 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

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

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 вместо пароля в открытом виде.

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 может быть сформирован как результат кодирования 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 (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