From 902e4cbfbc8e7e12e084aabb29c2b854f5ed4784 Mon Sep 17 00:00:00 2001 From: Tony Brown Date: Tue, 18 Aug 2015 13:06:39 +1000 Subject: [PATCH 1/2] Added support for RFC2617 MD5-sess algorithm type --- features/digest_authentication.feature | 10 +++++++ features/steps/mongrel_helper.rb | 33 ++++++++++++++++++++++ features/steps/remote_service_steps.rb | 4 +++ lib/httparty/net_digest_auth.rb | 20 +++++++++++-- spec/httparty/net_digest_auth_spec.rb | 39 ++++++++++++++++++++++++++ 5 files changed, 104 insertions(+), 2 deletions(-) diff --git a/features/digest_authentication.feature b/features/digest_authentication.feature index 456a0a7..83e8a60 100644 --- a/features/digest_authentication.feature +++ b/features/digest_authentication.feature @@ -18,3 +18,13 @@ Feature: Digest Authentication | username | password | | jcash | maninblack | Then the return value should match 'Digest Authenticated Page' + + Scenario: Passing proper credentials to a page requiring Digest Authentication using md5-sess algorithm + Given a remote service that returns 'Digest Authenticated Page Using MD5-sess' + And that service is accessed at the path '/digest_auth.html' + And that service is protected by MD5-sess Digest Authentication + And that service requires the username 'jcash' with the password 'maninblack' + When I call HTTParty#get with '/digest_auth.html' and a digest_auth hash: + | username | password | + | jcash | maninblack | + Then the return value should match 'Digest Authenticated Page Using MD5-sess' diff --git a/features/steps/mongrel_helper.rb b/features/steps/mongrel_helper.rb index 99713a5..66a2151 100644 --- a/features/steps/mongrel_helper.rb +++ b/features/steps/mongrel_helper.rb @@ -88,6 +88,39 @@ module DigestAuthentication end end +module DigestAuthenticationUsingMD5Sess + + EXPECTED_PASSWORD = 'maninblack' + + def self.extended(base) + base.custom_headers["WWW-Authenticate"] = 'Digest realm="testrealm@host.com",qop="auth,auth-int",algorithm="MD5-sess",nonce="nonce",opaque="opaque"' + end + + def process(request, response) + if authorized?(request) + super + else + reply_with(response, 401, "Incorrect. You have 20 seconds to comply.") + end + end + + def md5(str) + Digest::MD5.hexdigest(str) + end + + def authorized?(request) + auth = request.params["HTTP_AUTHORIZATION"] + params = {} + auth.to_s.gsub(/(\w+)="(.*?)"/) { params[$1] = $2 }.gsub(/(\w+)=([^,]*)/) { params[$1] = $2 } + a1a = [params['username'],params['realm'],EXPECTED_PASSWORD].join(':') + a1 = [md5(a1a),params['nonce'],params['cnonce'] ].join(':') + a2 = "GET:#{params['uri']}" + expected_response = md5( [md5(a1),params['nonce'], params['nc'], params['cnonce'], params['qop'],md5(a2)].join(':') ) + expected_response == params['response'] + end +end + + def new_mongrel_redirector(target_url, relative_path = false) target_url = "http://#{@host_and_port}#{target_url}" unless relative_path Mongrel::RedirectHandler.new(target_url) diff --git a/features/steps/remote_service_steps.rb b/features/steps/remote_service_steps.rb index 02ba5b1..e1aac14 100644 --- a/features/steps/remote_service_steps.rb +++ b/features/steps/remote_service_steps.rb @@ -55,6 +55,10 @@ Given /that service is protected by Digest Authentication/ do @handler.extend DigestAuthentication end +Given /that service is protected by MD5-sess Digest Authentication/ do + @handler.extend DigestAuthenticationUsingMD5Sess +end + Given /that service requires the username '(.*)' with the password '(.*)'/ do |username, password| @handler.username = username @handler.password = password diff --git a/lib/httparty/net_digest_auth.rb b/lib/httparty/net_digest_auth.rb index 4db9571..e124583 100644 --- a/lib/httparty/net_digest_auth.rb +++ b/lib/httparty/net_digest_auth.rb @@ -41,6 +41,8 @@ module Net %(response="#{request_digest}") ] + header << %(algorithm="#{@response['algorithm']}") if algorithm_present? + if qop_present? fields = [ %(cnonce="#{@cnonce}"), @@ -66,7 +68,8 @@ module Net header =~ /Digest (.*)/ params = {} - $1.gsub(/(\w+)="(.*?)"/) { params[$1] = $2 } + non_quoted = $1.gsub(/(\w+)="(.*?)"/) { params[$1] = $2 } + non_quoted.gsub(/(\w+)=([^,]*)/) { params[$1] = $2 } params end @@ -105,8 +108,21 @@ module Net Digest::MD5.hexdigest(str) end + def algorithm_present? + @response.key?('algorithm') && !@response['algorithm'].empty? + end + + def use_md5_sess? + algorithm_present? && @response['algorithm'] == 'MD5-sess' + end + def a1 - [@username, @response['realm'], @password].join(":") + a1_user_realm_pwd = [@username, @response['realm'], @password].join(':') + if use_md5_sess? + [ md5(a1_user_realm_pwd), @response['nonce'], @cnonce ].join(':') + else + a1_user_realm_pwd + end end def a2 diff --git a/spec/httparty/net_digest_auth_spec.rb b/spec/httparty/net_digest_auth_spec.rb index b18643a..2bf1d5f 100644 --- a/spec/httparty/net_digest_auth_spec.rb +++ b/spec/httparty/net_digest_auth_spec.rb @@ -188,4 +188,43 @@ RSpec.describe Net::HTTPHeader::DigestAuthenticator do expect(authorization_header).to include(%(response="#{request_digest}")) end end + + context "with algorithm specified" do + before do + @digest = setup_digest({ + 'www-authenticate' => 'Digest realm="myhost@testrealm.com", nonce="NONCE", qop="auth", algorithm=MD5' + }) + end + + it "should recognise algorithm was specified" do + expect( @digest.send :algorithm_present? ).to be(true) + end + + it "should set the algorithm header" do + expect(authorization_header).to include('algorithm="MD5"') + end + end + + context "with md5-sess algorithm specified" do + before do + @digest = setup_digest({ + 'www-authenticate' => 'Digest realm="myhost@testrealm.com", nonce="NONCE", qop="auth", algorithm=MD5-sess' + }) + end + + it "should recognise algorithm was specified" do + expect( @digest.send :algorithm_present? ).to be(true) + end + + it "should set the algorithm header" do + expect(authorization_header).to include('algorithm="MD5-sess"') + end + + it "should set response using md5-sess algorithm" do + request_digest = "md5(md5(md5(Mufasa:myhost@testrealm.com:Circle Of Life):NONCE:md5(deadbeef)):NONCE:00000001:md5(deadbeef):auth:md5(GET:/dir/index.html))" + expect(authorization_header).to include(%(response="#{request_digest}")) + end + + end + end From c9f868694a9571503f0266eb9323f2fa3f8f6a84 Mon Sep 17 00:00:00 2001 From: Tony Brown Date: Tue, 18 Aug 2015 14:13:30 +1000 Subject: [PATCH 2/2] Improved digest MD5-sess test --- features/steps/mongrel_helper.rb | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/features/steps/mongrel_helper.rb b/features/steps/mongrel_helper.rb index 66a2151..3688b7e 100644 --- a/features/steps/mongrel_helper.rb +++ b/features/steps/mongrel_helper.rb @@ -89,11 +89,11 @@ module DigestAuthentication end module DigestAuthenticationUsingMD5Sess - - EXPECTED_PASSWORD = 'maninblack' - + NONCE = 'nonce' + REALM = 'testrealm@host.com' + QOP = 'auth,auth-int' def self.extended(base) - base.custom_headers["WWW-Authenticate"] = 'Digest realm="testrealm@host.com",qop="auth,auth-int",algorithm="MD5-sess",nonce="nonce",opaque="opaque"' + base.custom_headers["WWW-Authenticate"] = %(Digest realm="#{REALM}",qop="#{QOP}",algorithm="MD5-sess",nonce="#{NONCE}",opaque="opaque"') end def process(request, response) @@ -109,14 +109,14 @@ module DigestAuthenticationUsingMD5Sess end def authorized?(request) - auth = request.params["HTTP_AUTHORIZATION"] - params = {} - auth.to_s.gsub(/(\w+)="(.*?)"/) { params[$1] = $2 }.gsub(/(\w+)=([^,]*)/) { params[$1] = $2 } - a1a = [params['username'],params['realm'],EXPECTED_PASSWORD].join(':') - a1 = [md5(a1a),params['nonce'],params['cnonce'] ].join(':') - a2 = "GET:#{params['uri']}" - expected_response = md5( [md5(a1),params['nonce'], params['nc'], params['cnonce'], params['qop'],md5(a2)].join(':') ) - expected_response == params['response'] + auth = request.params["HTTP_AUTHORIZATION"] + params = {} + auth.to_s.gsub(/(\w+)="(.*?)"/) { params[$1] = $2 }.gsub(/(\w+)=([^,]*)/) { params[$1] = $2 } + a1a = [@username,REALM,@password].join(':') + a1 = [md5(a1a),NONCE,params['cnonce'] ].join(':') + a2 = [ request.params["REQUEST_METHOD"], request.params["REQUEST_URI"] ] .join(':') + expected_response = md5( [md5(a1), NONCE, params['nc'], params['cnonce'], QOP, md5(a2)].join(':') ) + expected_response == params['response'] end end