Skip to content

Commit

Permalink
Merge pull request #98 from fhars/digest-auth
Browse files Browse the repository at this point in the history
Digest auth changes

(Awesome contributions for new feature and existing tests. Thanks!)
  • Loading branch information
Hiroshi Nakamura committed Aug 11, 2012
2 parents ed9c65c + dd25606 commit 06a4c3d
Show file tree
Hide file tree
Showing 2 changed files with 201 additions and 11 deletions.
88 changes: 78 additions & 10 deletions lib/httpclient/auth.rb
Expand Up @@ -120,6 +120,10 @@ def filter_request(req)

# Filter API implementation. Traps HTTP response and parses
# 'WWW-Authenticate' header.
#
# This remembers the challenges for all authentication methods
# available to the client. On the subsequent retry of the request,
# filter_request will select the strongest method.
def filter_response(req, res)
command = nil
if res.status == HTTP::Status::UNAUTHORIZED
Expand Down Expand Up @@ -156,17 +160,19 @@ def filter_response(req, res)
# SSPINegotiateAuth requires 'win32/sspi' module.
class ProxyAuth < AuthFilterBase
attr_reader :basic_auth
attr_reader :digest_auth
attr_reader :negotiate_auth
attr_reader :sspi_negotiate_auth

# Creates new ProxyAuth.
def initialize
@basic_auth = BasicAuth.new
@basic_auth = ProxyBasicAuth.new
@negotiate_auth = NegotiateAuth.new
@ntlm_auth = NegotiateAuth.new('NTLM')
@sspi_negotiate_auth = SSPINegotiateAuth.new
@digest_auth = ProxyDigestAuth.new
# sort authenticators by priority
@authenticator = [@negotiate_auth, @ntlm_auth, @sspi_negotiate_auth, @basic_auth]
@authenticator = [@negotiate_auth, @ntlm_auth, @sspi_negotiate_auth, @digest_auth, @basic_auth]
end

# Resets challenge state. See sub filters for more details.
Expand Down Expand Up @@ -281,6 +287,25 @@ def challenge(uri, param_str = nil)
end
end

class ProxyBasicAuth < BasicAuth

def set(uri, user, passwd)
@set = true
@cred = ["#{user}:#{passwd}"].pack('m').tr("\n", '')
end

def get(req)
target_uri = req.header.request_uri
return nil unless @challengeable['challenged']
@cred
end

# Challenge handler: remember URL for response.
def challenge(uri, param_str = nil)
@challengeable['challenged'] = true
true
end
end

# Authentication filter for handling DigestAuth negotiation.
# Used in WWWAuth.
Expand Down Expand Up @@ -352,9 +377,12 @@ def calc_cred(req, user, passwd, param)
path = req.header.create_query_uri
a_1 = "#{user}:#{param['realm']}:#{passwd}"
a_2 = "#{method}:#{path}"
qop = param['qop']
nonce = param['nonce']
cnonce = generate_cnonce()
@nonce_count += 1
cnonce = nil
if qop || param['algorithm'] =~ /MD5-sess/
cnonce = generate_cnonce()
end
a_1_md5sum = Digest::MD5.hexdigest(a_1)
if param['algorithm'] =~ /MD5-sess/
a_1_md5sum = Digest::MD5.hexdigest("#{a_1_md5sum}:#{nonce}:#{cnonce}")
Expand All @@ -365,18 +393,25 @@ def calc_cred(req, user, passwd, param)
message_digest = []
message_digest << a_1_md5sum
message_digest << nonce
message_digest << ('%08x' % @nonce_count)
message_digest << cnonce
message_digest << param['qop']
if qop
@nonce_count += 1
message_digest << ('%08x' % @nonce_count)
message_digest << cnonce
message_digest << param['qop']
end
message_digest << Digest::MD5.hexdigest(a_2)
header = []
header << "username=\"#{user}\""
header << "realm=\"#{param['realm']}\""
header << "nonce=\"#{nonce}\""
header << "uri=\"#{path}\""
header << "cnonce=\"#{cnonce}\""
header << "nc=#{'%08x' % @nonce_count}"
header << "qop=#{param['qop']}"
if cnonce
header << "cnonce=\"#{cnonce}\""
end
if qop
header << "nc=#{'%08x' % @nonce_count}"
header << "qop=#{param['qop']}"
end
header << "response=\"#{Digest::MD5.hexdigest(message_digest.join(":"))}\""
header << "algorithm=#{algorithm}"
header << "opaque=\"#{param['opaque']}\"" if param.key?('opaque')
Expand Down Expand Up @@ -404,6 +439,39 @@ def parse_challenge_param(param_str)
end


# Authentication filter for handling DigestAuth negotiation.
# Ignores uri argument. Used in ProxyAuth.
class ProxyDigestAuth < DigestAuth

# overrides DigestAuth#set. sets default user name and password. uri is not used.
def set(uri, user, passwd)
@set = true
@auth = [user, passwd]
end

# overrides DigestAuth#get. Uses default user name and password
# regardless of target uri if the proxy has required authentication
# before
def get(req)
target_uri = req.header.request_uri
param = @challenge
return nil unless param
user, passwd = @auth
return nil unless user
calc_cred(req, user, passwd, param)
end

def reset_challenge
@challenge = nil
end

def challenge(uri, param_str)
@challenge = parse_challenge_param(param_str)
true
end

end

# Authentication filter for handling Negotiate/NTLM negotiation.
# Used in WWWAuth and ProxyAuth.
#
Expand Down
124 changes: 123 additions & 1 deletion test/test_auth.rb
@@ -1,5 +1,5 @@
require File.expand_path('helper', File.dirname(__FILE__))

require 'digest/md5'

class TestAuth < Test::Unit::TestCase
include Helper
Expand Down Expand Up @@ -56,6 +56,23 @@ def setup_server
:UserDB => htdigest_userdb
)
@server_thread = start_server_thread(@server)

@proxy_digest_auth = WEBrick::HTTPAuth::ProxyDigestAuth.new(
:Logger => @proxylogger,
:Algorithm => 'MD5',
:Realm => 'auth',
:UserDB => htdigest_userdb
)

@proxyserver = WEBrick::HTTPProxyServer.new(
:ProxyAuthProc => @proxy_digest_auth.method(:authenticate).to_proc,
:BindAddress => "localhost",
:Logger => @proxylogger,
:Port => 0,
:AccessLog => []
)
@proxyport = @proxyserver.config[:Port]
@proxyserver_thread = start_server_thread(@proxyserver)
end

def do_basic_auth(req, res)
Expand Down Expand Up @@ -105,12 +122,32 @@ def test_BASIC_auth
end
end

def test_basic_auth_reuses_credentials
c = HTTPClient.new
c.set_auth("http://localhost:#{serverport}/", 'admin', 'admin')
assert_equal('basic_auth OK', c.get_content("http://localhost:#{serverport}/basic_auth/"))
c.test_loopback_http_response << "HTTP/1.0 200 OK\nContent-Length: 2\n\nOK"
c.debug_dev = str = ''
c.get_content("http://localhost:#{serverport}/basic_auth/sub/dir/")
assert_match /Authorization: Basic YWRtaW46YWRtaW4=/, str
end

def test_digest_auth
c = HTTPClient.new
c.set_auth("http://localhost:#{serverport}/", 'admin', 'admin')
assert_equal('digest_auth OK', c.get_content("http://localhost:#{serverport}/digest_auth"))
end

def test_digest_auth_reuses_credentials
c = HTTPClient.new
c.set_auth("http://localhost:#{serverport}/", 'admin', 'admin')
assert_equal('digest_auth OK', c.get_content("http://localhost:#{serverport}/digest_auth/"))
c.test_loopback_http_response << "HTTP/1.0 200 OK\nContent-Length: 2\n\nOK"
c.debug_dev = str = ''
c.get_content("http://localhost:#{serverport}/digest_auth/sub/dir/")
assert_match /Authorization: Digest/, str
end

def test_digest_auth_with_block
c = HTTPClient.new
c.set_auth("http://localhost:#{serverport}/", 'admin', 'admin')
Expand Down Expand Up @@ -147,6 +184,16 @@ def test_digest_auth_with_querystring
assert_equal('digest_auth OKbar=baz', c.get_content("http://localhost:#{serverport}/digest_auth/foo?bar=baz"))
end

def test_perfer_digest
c = HTTPClient.new
c.set_auth('http://example.com/', 'admin', 'admin')
c.test_loopback_http_response << "HTTP/1.0 401 Unauthorized\nWWW-Authenticate: Basic realm=\"foo\"\nWWW-Authenticate: Digest realm=\"foo\", nonce=\"nonce\", stale=false\nContent-Length: 2\n\nNG"
c.test_loopback_http_response << "HTTP/1.0 200 OK\nContent-Length: 2\n\nOK"
c.debug_dev = str = ''
c.get_content('http://example.com/')
assert_match(/^Authorization: Digest/, str)
end

def test_digest_sess_auth
c = HTTPClient.new
c.set_auth("http://localhost:#{serverport}/", 'admin', 'admin')
Expand All @@ -163,6 +210,81 @@ def test_proxy_auth
assert_match(/Proxy-Authorization: Basic YWRtaW46YWRtaW4=/, str)
end

def test_proxy_auth_reuses_credentials
c = HTTPClient.new
c.set_proxy_auth('admin', 'admin')
c.test_loopback_http_response << "HTTP/1.0 407 Unauthorized\nProxy-Authenticate: Basic realm=\"foo\"\nContent-Length: 2\n\nNG"
c.test_loopback_http_response << "HTTP/1.0 200 OK\nContent-Length: 2\n\nOK"
c.test_loopback_http_response << "HTTP/1.0 200 OK\nContent-Length: 2\n\nOK"
c.get_content('http://www1.example.com/')
c.debug_dev = str = ''
c.get_content('http://www2.example.com/')
assert_match(/Proxy-Authorization: Basic YWRtaW46YWRtaW4=/, str)
end

def test_digest_proxy_auth_loop
c = HTTPClient.new
c.set_proxy_auth('admin', 'admin')
c.test_loopback_http_response << "HTTP/1.0 407 Unauthorized\nProxy-Authenticate: Digest realm=\"foo\", nonce=\"nonce\", stale=false\nContent-Length: 2\n\nNG"
c.test_loopback_http_response << "HTTP/1.0 200 OK\nContent-Length: 2\n\nOK"
md5 = Digest::MD5.new
ha1 = md5.hexdigest("admin:foo:admin")
ha2 = md5.hexdigest("GET:/")
response = md5.hexdigest("#{ha1}:nonce:#{ha2}")
c.debug_dev = str = ''
c.get_content('http://example.com/')
assert_match(/Proxy-Authorization: Digest/, str)
assert_match(%r"response=\"#{response}\"", str)
end

def test_digest_proxy_auth
c=HTTPClient.new("http://localhost:#{proxyport}/")
c.set_proxy_auth('admin', 'admin')
c.set_auth("http://127.0.0.1:#{serverport}/", 'admin', 'admin')
assert_equal('basic_auth OK', c.get_content("http://127.0.0.1:#{serverport}/basic_auth"))
end

def test_digest_proxy_invalid_auth
c=HTTPClient.new("http://localhost:#{proxyport}/")
c.set_proxy_auth('admin', 'wrong')
c.set_auth("http://127.0.0.1:#{serverport}/", 'admin', 'admin')
assert_raises(HTTPClient::BadResponseError) do
c.get_content("http://127.0.0.1:#{serverport}/basic_auth")
end
end

def test_prefer_digest_to_basic_proxy_auth
c = HTTPClient.new
c.set_proxy_auth('admin', 'admin')
c.test_loopback_http_response << "HTTP/1.0 407 Unauthorized\nProxy-Authenticate: Digest realm=\"foo\", nonce=\"nonce\", stale=false\nProxy-Authenticate: Basic realm=\"bar\"\nContent-Length: 2\n\nNG"
c.test_loopback_http_response << "HTTP/1.0 200 OK\nContent-Length: 2\n\nOK"
md5 = Digest::MD5.new
ha1 = md5.hexdigest("admin:foo:admin")
ha2 = md5.hexdigest("GET:/")
response = md5.hexdigest("#{ha1}:nonce:#{ha2}")
c.debug_dev = str = ''
c.get_content('http://example.com/')
assert_match(/Proxy-Authorization: Digest/, str)
assert_match(%r"response=\"#{response}\"", str)
end

def test_digest_proxy_auth_reuses_credentials
c = HTTPClient.new
c.set_proxy_auth('admin', 'admin')
c.test_loopback_http_response << "HTTP/1.0 407 Unauthorized\nProxy-Authenticate: Digest realm=\"foo\", nonce=\"nonce\", stale=false\nContent-Length: 2\n\nNG"
c.test_loopback_http_response << "HTTP/1.0 200 OK\nContent-Length: 2\n\nOK"
c.test_loopback_http_response << "HTTP/1.0 200 OK\nContent-Length: 2\n\nOK"
md5 = Digest::MD5.new
ha1 = md5.hexdigest("admin:foo:admin")
ha2 = md5.hexdigest("GET:/")
response = md5.hexdigest("#{ha1}:nonce:#{ha2}")
c.get_content('http://www1.example.com/')
c.debug_dev = str = ''
c.get_content('http://www2.example.com/')
assert_match(/Proxy-Authorization: Digest/, str)
assert_match(%r"response=\"#{response}\"", str)
end

def test_oauth
c = HTTPClient.new
config = HTTPClient::OAuth::Config.new(
Expand Down

0 comments on commit 06a4c3d

Please sign in to comment.