Spec-Zone.ru › Ruby on Rails 7.1

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

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

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

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

authenticate(request, realm, &password_procedure) Показать исходный код
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 211
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 269
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 276
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 262
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 258
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 253
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 243
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 249
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 321
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)

где временная метка — сгенерированное сервером время или другое не повторяющееся значение, ETag — значение заголовка HTTP ETag, связанного с запрашиваемым объектом, и private-key — данные, известные только серверу. При таком 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 340
def opaque(secret_key)
  OpenSSL::Digest::MD5.hexdigest(secret_key)
end

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

secret_token(request) Показать исходный код
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 283
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 218
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 333
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