SVX日記
2024-09-10(Tue) RubyでDigest認証クライアントを作る
あれ。Rubyの標準ライブラリにはBasic認証の機能しかない。TLSの普及につれ、Digest認証が廃れて、Basic認証が復活してくる傾向とは思っていたが、相手サーバがDigest認証で固定なら、それに対応せざるをえない。
Digest認証が面倒なのは、認証に先立って相手からnonceというチャレンジコードをもらう必要があるので、最初に失敗アクセス(401:Unauthorized)を行うことが必須であることだ。Basic認証ならば、最初から認証情報を送ることができるので、失敗アクセスは必須ではない。
RFCを眺めつつ、既存のRubyのライブラリに近い使い勝手で実装する。できた。
require './http_digest_auth'
res = nil; auth_res = nil
uri = URI.parse('http://localhost:8080/digest/')
req = Net::HTTP::Get.new(uri.path)
3.times {
auth_res = req.digest_auth('username', 'password', res, auth_res)
res = Net::HTTP.start(uri.host, uri.port) {|http|
http.request(req)
}
puts(res.code)
res.code.to_i < 400 and break
}
puts(res.body)
既存のRubyのライブラリでBasic認証する場合のサンプルコードが以下なので、できるだけ近づけた。ただし、最初に失敗アクセスを行うことが必須なので、ループしたり、認証情報の引き渡したりする処理が入るのは仕方ない。逆に、上記のスタイルでもBasic認証は動く。
require 'net/http'
uri = URI.parse('http://localhost:8080/basic/')
req = Net::HTTP::Get.new(uri.path)
req.basic_auth('username', 'password')
res = Net::HTTP.start(uri.host, uri.port) {|http|
http.request(req)
}
puts(res.body)
require 'net/http'
require 'digest/md5'
class Net::HTTPGenericRequest
attr_accessor :secret_data
def digest_auth(username, passwd, res, auth_res = nil)
@nc ||= 0
@secret_data ||= 'secret-data'
@auth_res = auth_res || {}
if(res and res.header['www-authenticate'] =~ /Digest\s+(.+)/i)
$1.split(/,\s*/).each {|kv0|
kv = kv0.split(/\s*=\s*/, 2)
kv[1].strip =~ /^\"(.*)\"$/ and kv[1] = $1
@auth_res[kv[0]] = kv[1]
}
end
if(@auth_res['realm'])
ha1 = Digest::MD5.hexdigest('%s:%s:%s' % [username, @auth_res['realm'], passwd])
ha2 = Digest::MD5.hexdigest('%s:%s' % [method, path])
nc = '%08X' % [@nc += 1]
cnonce = Digest::MD5.hexdigest('%s:%s' % [Time.now.to_f, @secret_data])
qop = 'auth' # TODO
response = Digest::MD5.hexdigest('%s:%s:%s:%s:%s:%s' % [ha1, @auth_res['nonce'], nc, cnonce, qop, ha2])
auth_req = []
auth_req << 'username="%s"' % username
auth_req << 'realm="%s"' % @auth_res['realm']
auth_req << 'uri="%s"' % path
auth_req << 'algorithm=%s' % 'MD5' # TODO
auth_req << 'nonce="%s"' % @auth_res['nonce']
auth_req << 'nc=%s' % nc
auth_req << 'cnonce="%s"' % cnonce
auth_req << 'qop=%s' % qop
auth_req << 'response="%s"' % response
auth_req << 'opaque="%s"' % @auth_res['opaque'] if(@auth_res['opaque'])
@header['authorization'] = ['Digest ' + auth_req.join(', ')]
end
@auth_res
end
end