前提
- Rails の話
- 下記のような2組の ID/PW ペアが存在するとする
- user1 / pass1
- user2 / pass2
やりたいこと
- あるアクションに対して、 user1 または user2 だけがアクセスできるような BASIC 認証をかけたい
結論
以下のように実装すればよい。
class FooController < ApplicationController
before_action :multiple_authenticate
def index
end
protected
def multiple_authenticate
authenticate_or_request_with_http_basic do |username, password|
(
ActiveSupport::SecurityUtils.secure_compare(username, 'user1') &
ActiveSupport::SecurityUtils.secure_compare(password, 'pass1')
) | (
ActiveSupport::SecurityUtils.secure_compare(username, 'user2') &
ActiveSupport::SecurityUtils.secure_compare(password, 'pass2')
)
end
end
end
もちろん、ID/PW をハードコードしているところは適宜セキュアにすること(環境変数を使う、secret.yml を使うなど)。
解説
まず、「やりたいこと」で示した「OR 条件による BASIC 認証」を実現する標準機能はどうやら存在しないようでした。
仕方ないので自前で実装するわけですが、ここで普通に BASIC 認証するときに使う http_basic_authenticate_with
の実装を参考にします。ソースはこちら (https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/metal/http_authentication.rb#L71-L81) 。
このメソッドは、直下で定義されている authenticate_or_request_with_http_basic
というメソッド(名前が似ていてややこしい)を使って before_action
を設定しているだけのようです。authenticate_or_request_with_http_basic
は、
- リクエストヘッダに BASIC 認証情報が含まれていなければ 401 を返す(ID / PW を聞く)
- リクエストヘッダに BASIC 認証情報が含まれていればブロックを評価し、
- truthy なら処理を続行
- falsy なら 401 を返す(ID / PW を聞く)
という挙動をします。ということは、authenticate_or_request_with_http_basic
のブロック内で user1 または user2 の ID/PW と一致するかどうかをチェックすればよい、ということになります。
ここで、http_basic_authenticate_with
のソースに目を戻します(繰り返しになりますが、メソッド名がややこしいので混乱しないよう注意してください)。この実装を見ると、ID/PW の比較に変わったメソッドを使っています。
# 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(name, options[:name]) &
ActiveSupport::SecurityUtils.secure_compare(password, options[:password])
ここでは、セキュリティのために以下の2つの工夫がこらされています。
- 文字列の比較に
secure_compare
を使っている- コメントにも書かれていますが、これによって比較対象の文字長がバレないようになっています。
- 文字列比較が「そもそも文字長が一致してなければ即エラーを返す」という実装になっている場合、比較処理がすぐ終わるかどうかで文字長を推理できてしまいます。いわゆるタイミング攻撃というものです。
- 実装を見てみると、与えられた文字列を SHA256 でハッシュ化してから比較しています。つまり常に 256 文字同士の比較になるため、セキュアというわけです。
- 参考: https://github.com/rails/rails/commit/fa487763d98ccf9c3e66fdb44f09af5c37a50fe5
- AND 演算に
&
を使っている- コメントにも書かれていますが、これによって短絡評価がされないようになっています。
- 短絡評価されてしまうと、
name
が正しいときと間違っているときとで処理時間に差が出てしまい、ここでもタイミング攻撃のリスクが発生します。 - 参考: https://github.com/rails/rails/commit/17e6f1507b7f2c2a883c180f4f9548445d6dfbda
これに倣うと、今回実装する複数 ID/PW ペアに対する OR 条件での認証も、
- 文字列の比較に
secure_compare
を使う - OR 条件に
|
を使う
という方針で実装すればよさそうです。その実装例が上記で示したものとなっています。