手元で作った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
を呼ぶ実装になっている。
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
を利用している。
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|
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
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
を呼ぶ実装でも良い。
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
基本認証を要求するレスポンスを返す処理。
def request_http_basic_authentication(realm = "Application", message = nil)
HttpAuthentication::Basic.authentication_request(self, realm, message)
end
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を呼んで入力させたい場合。 |