Posted at

Rails で、複数 ID に対して OR 条件で BASIC 認証をかける方法

More than 1 year has passed since last update.


前提


  • 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 条件に | を使う

という方針で実装すればよさそうです。その実装例が上記で示したものとなっています。