Rack::Auth::Digest::MD5 のつかいかた
Rack::Auth::Digest::MD5
は opaque
を渡さないといけない((つうかそもそも opaque
は optional なはずなのに Rack::Auth::Digest::MD5
では必須パラメータってのも変なんだけど。))ので素直に書けないと思いがちだけど,現在の Rack::Auth::Digest::MD5
は第2引数に opaque
をとるので,シンプルに use
を使って書ける。
require 'rack/auth/digest/md5' use Rack::Auth::Digest::MD5, 'my realm', '', do |username| 'password' end run my_app
Padrino の場合はこんなふうに。
Padrino.before_load do require 'rack/auth/digest/md5' Padrino.use Rack::Auth::Digest::MD5, 'my realm', '', do |username| 'password' end end
アプリケーションのステートに応じて opaque
を返したい場合((これが本来の opaque
の使い方。とはいえ,いろいろ代替手段があるので (nonce
に入れ込んじゃうとか Cookie 使うとか) opaque
が真面目に使われるケースはないんじゃないかな。))は,結局 Rack::Auth::Digest::MD5
のインスタンスを生成して rack mount していくしかない気がする。そもそもそんなシチュエーションでは rack middleware じゃ単純には無理かな?
ともかく。
実は名前付きパラメータでも引数を渡せるので下記のようにも書ける。
require 'rack/auth/digest/md5' use Rack::Auth::Digest::MD5, { :realm => 'the realm', :opaque => '', }, do |username| 'password' end run my_app
ところで。
せっかく HTTP Digest 認証なのに,生パスワードを書かないといけないなんてダッサいと思いませんか。
:passwords_hashed
パラメータを true
にすると,ハッシュ化したパスワード等 (いわゆる A1
) を渡せばよくなるのでシステムに生パスワードを保存しておく必要がなくなる((実はハッシュ化された A1
(と username
の組) を盗まれると認証できてしまう。なのでハッシュ化して保存したとしてもユーザーの (他のサービスと共用しているかもしれない) 生パスワードが盗まれないという意義しかない。))。
require 'rack/auth/digest/md5' REALM = 'the realm' require 'digest/md5' PWHASH = Digest::MD5.new.update('%s:%s:%s' % ['dayflower', REALM, 'password']) use Rack::Auth::Digest::MD5, { :realm => REALM, :opaque => '', :passwords_hashed => true, }, do |username| PWHASH end run my_app
PWHASH
の算出のところに生パスワード書いてあるけどこれはあくまでサンプルだからであって,あらかじめ計算しておくなり,htdigest
コマンドで生成した値を利用するなり,データベースに保存しておくなり,しておけば生パスワードを保存しておく必要はなくなる。
Rack::Auth::Digest::MD5 での nonce のとりあつかい (とバグ)
一般に,HTTP Digest 認証でリプレイ攻撃を「厳密に」防ぐには
nonce
をサーバサイドで生成しサーバに保持しておく- クライアントから返された
nonce
とnc
の組がすでに認証済ならハネる (新しいnonce
を生成し,stale
を立ててレスポンスするだけでいい) - クライアントから返された
nonce
がサーバサイドに保持したものと違えばハネる (上記と同様stale
token とする)
などする必要がある。
Rack::Auth::Digest::MD5
では nonce
はタイムスタンプになっている。のでサーバサイドに nonce
を保持しておく必要がなくなり実装が楽である。
「一定時間」を超えた nonce
を破棄していけば,その一定時間を超えた段階でのリプレイ攻撃が成立しなくなるのでカジュアルには,悪い選択肢ではない*1。
Rack::Auth::Digest::MD5
ではデフォルトではこの「一定時間」が設定されていない。このことは nonce
が破棄されないことをしめしている。なのでリプレイ攻撃やり放題である (サーバサイドに何も保持していないので nc
のチェックもしてないし)。
「一定時間」を設定するには Rack::Auth::Digest::Nonce::time_limit
に値を設定する。
require 'rack/auth/digest/nonce' Rack::Auth::Digest::Nonce::time_limit = 10
単位は秒なので,この例でいくとサーバが nonce
を発行してから10秒経つと無効な nonce
となる。時間切れになった場合は,stale
属性の立った WWW-Authenticate
をサーバが返すので,ユーザーエージェント側でパスワード入力ダイアログが再び出ることはない。エージェントが新しい nonce
をもとに自動的に認証ダイジェストを計算して再送信することになる((なので time_limit
をかなり小さくしてもまぁ大丈夫なのだが,サーバとクライアント間の通信遅延が大きい場合やクライアントの処理能力が非常に低い場合などはそれなりに大きなものにしておく必要があるだろう。))。
と,これでいいはずなんだけど,現在のところ Rack::Auth::Digest::Nonce
にバグがあるので,このようにすると常に stale
となり延々と認証リクエストレスポンスネゴシエーションが走ってしまう。このバグに対処するには下記のようにすればよい。
require 'rack/auth/digest/nonce' class Rack::Auth::Digest::Nonce def stale? !self.class.time_limit.nil? && (Time.now.to_i - @timestamp) > self.class.time_limit end end
誰も使っていないフィーチャーなのだろう。そもそも安全でない経路で Digest 認証をやることがメジャーではないのかもしれない。
暇ができたら issue 立てて pull request するつもりだけど,テストまで込みで考えるとめんどいなぁ。pull request だしといた。master にマージされた。