Spec-Zone.ru › Ruby on Rails 4.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(запрос, домен, &процедура_пароля) Показать исходный код
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 194
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 252
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 259
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 245
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 241
def decode_credentials_header(request)
  decode_credentials(request.authorization)
end
encode_credentials(http_метод, учетные_данные, пароль, пароль_является_ha1) Показать исходный код
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 236
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 226
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 232
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 304
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(секретный_ключ) Показать исходный код
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 323
def opaque(secret_key)
  ::Digest::MD5.hexdigest(secret_key)
end

Непрозрачный, основанный на случайной генерации — но меняется ли каждый запрос?

secret_token(запрос) Показать исходный код
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 266
def secret_token(request)
  key_generator  = request.env["action_dispatch.key_generator"]
  http_auth_salt = request.env["action_dispatch.http_auth_salt"]
  key_generator.generate_key(http_auth_salt)
end
validate_digest_response(запрос, домен, &процедура_пароля) Показать исходный код
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 201
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.env['rack.methodoverride.original_method'] || request.env['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(секретный_ключ, запрос, значение, секунды_до_таймаута=5*60) Показать исходный код
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 316
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