手元で作ったRailsアプリを軽くデプロイして確認したかったので、基本認証入れて確認しようとしていましたが、 似たようなメソッドがいくつもあってどれだっけとなったり、良くない実装が検索結果に出てきたので、改めて整理しておきます。
ググると
以下のようなコードが紹介されています。
class ApplicationController < ActionController::Base before_action :basic_auth def basic_auth authenticate_or_request_with_http_basic do |username, password| username == ENV["BASIC_AUTH_USER"] && password == ENV["BASIC_AUTH_PASSWORD"] end end
==
での比較は timing attacks の脆弱性があります。
secure_compare使おう
secure_compare を使うというのが解なのですが、こちらの記事がめちゃくちゃ詳しく載っててるので割愛します。
参考)機密情報に関わる文字列の比較は == ではなく secure_compare を使おう
やり方おさらい
やり方1. http_basic_authenticate_with
simpleなやつ。こんな感じで使います。簡単で良いですね。
class PostsController < ApplicationController http_basic_authenticate_with name: "dhh", password: "secret", except: :index
内部的には、before_actionで http_basic_authenticate_or_request_with
を呼ぶ実装になっている。
# https://github.com/rails/rails/blob/v7.0.4/actionpack/lib/action_controller/metal/http_authentication.rb#L76 def http_basic_authenticate_with(name:, password:, realm: nil, **options) raise ArgumentError, "Expected name: to be a String, got #{name.class}" unless name.is_a?(String) raise ArgumentError, "Expected password: to be a String, got #{password.class}" unless password.is_a?(String) before_action(options) { http_basic_authenticate_or_request_with name: name, password: password, realm: realm } end
そして、http_basic_authenticate_or_request_with
では、ActiveSupport::SecurityUtils.secure_compare
を利用している。
# https://github.com/rails/rails/blob/v7.0.4/actionpack/lib/action_controller/metal/http_authentication.rb#L83 def http_basic_authenticate_or_request_with(name:, password:, realm: nil, message: nil) authenticate_or_request_with_http_basic(realm, message) do |given_name, given_password| # This comparison uses & so that it doesn't short circuit and # uses `secure_compare` so that length information isn't leaked. ActiveSupport::SecurityUtils.secure_compare(given_name.to_s, name) & ActiveSupport::SecurityUtils.secure_compare(given_password.to_s, password) end end
やり方2.http_basic_authenticate_or_request_with
独自の処理を噛ませたい場合は、当然ながら「1」で呼んでいる http_basic_authenticate_or_request_with
を利用することで実現できる。
class ApplicationController < ActionController::Base before_action :set_account, :authenticate private def authenticate # do something http_basic_authenticate_or_request_with(name: "dhh", password: "secret") end
やり方3. authenticate_or_request_with_http_basic
また、当然ながら2で呼んでる authenticate_or_request_with_http_basic
を呼ぶ実装でも良い。
# https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/actionpack/lib/action_controller/metal/http_authentication.rb#L92 def authenticate_or_request_with_http_basic(realm = nil, message = nil, &login_procedure) authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm || "Application", message) end def authenticate_with_http_basic(&login_procedure) HttpAuthentication::Basic.authenticate(request, &login_procedure) end
やり方4. authenticate_with_http_basic
またまた当然ながら3で呼んでる authenticate_with_http_basic
を呼ぶ実装でも良い。
この場合は、基本認証を要求するレスポンスが自動で返らないので、必要に応じて request_http_basic_authentication
(後述)を呼び出したりする必要がある。
Advanced Basic exampleとしてRailsのドキュメントにはこの例が記載されている。
class ApplicationController < ActionController::Base before_action :set_account, :authenticate private def set_account @account = Account.find_by(url_name: request.subdomains.first) end def authenticate case request.format when Mime[:xml], Mime[:atom] if user = authenticate_with_http_basic { |u, p| @account.users.authenticate(u, p) } @current_user = user else request_http_basic_authentication end else if session_authenticated? @current_user = @account.users.find(session[:authenticated][:user_id]) else redirect_to(login_url) and return false end end end end
request_http_basic_authentication
基本認証を要求するレスポンスを返す処理。
# https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/actionpack/lib/action_controller/metal/http_authentication.rb#L100 def request_http_basic_authentication(realm = "Application", message = nil) HttpAuthentication::Basic.authentication_request(self, realm, message) end
# https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/actionpack/lib/action_controller/metal/http_authentication.rb#L100 def authentication_request(controller, realm, message) message ||= "HTTP Basic: Access denied.\n" controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.tr('"', "")}") controller.status = 401 controller.response_body = message end
まとめ
色々あってややこしいので軽くまとめておく。
No. | メソッド名 | secure_compareの利用の有無 | 認証不可時に基本認証を要求するレスポンスを返すかどうか | 備考 |
---|---|---|---|---|
1 | http_basic_authenticate_with | 有 | 返す | 単に基本認証するだけならこちらがおすすめ。 |
2 | http_basic_authenticate_or_request_with | 有 | 返す | 独自の処理を追加したいのであれば、こちらがおすすめ。認証不可の場合は、 基本認証を要求するレスポンスを返す。 |
3 | authenticate_or_request_with_http_basic | 無 | 返す | secure_compare が不要なケース。 |
4 | authenticate_with_http_basic | 無 | 返さない | ブラウザからの入力を期待しないケース。もしくは、別途任意のタイミングでrequest_http_basic_authenticationを呼んで入力させたい場合。 |