From 3e080c3b8c021e994d5763d85dcd4a4b0bc5e1a6 Mon Sep 17 00:00:00 2001 From: Thimo Emmerich Date: Tue, 7 Apr 2020 11:51:00 +0200 Subject: [PATCH 1/2] Added Kerberos proxy authentication based on code published here: https://github.com/requests/requests-kerberos/issues/148 --- requests_kerberos/kerberos_.py | 46 +++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/requests_kerberos/kerberos_.py b/requests_kerberos/kerberos_.py index 86e31c6..c5c3feb 100644 --- a/requests_kerberos/kerberos_.py +++ b/requests_kerberos/kerberos_.py @@ -85,7 +85,10 @@ def _negotiate_value(response): regex = re.compile(r'(?:.*,)*\s*Negotiate\s*([^,]*),?', re.I) _negotiate_value.regex = regex - authreq = response.headers.get('www-authenticate', None) + if response.status_code == 407: + authreq = response.headers.get('proxy-authenticate', None) + else: + authreq = response.headers.get('www-authenticate', None) if authreq: match_obj = regex.search(authreq) @@ -256,6 +259,9 @@ def authenticate_user(self, response, **kwargs): """Handles user authentication with gssapi/kerberos""" host = urlparse(response.url).hostname + if response.status_code == 407: + if 'proxies' in kwargs and urlparse(response.url).scheme in kwargs['proxies']: + host = urlparse(kwargs['proxies'][urlparse(response.url).scheme]).hostname try: auth_header = self.generate_request_header(response, host) @@ -263,9 +269,14 @@ def authenticate_user(self, response, **kwargs): # GSS Failure, return existing response return response - log.debug("authenticate_user(): Authorization header: {0}".format( - auth_header)) - response.request.headers['Authorization'] = auth_header + if response.status_code == 407: + log.debug("authenticate_user(): Proxy-Authorization header: {0}".format( + auth_header)) + response.request.headers['Proxy-Authorization'] = auth_header + else: + log.debug("authenticate_user(): Authorization header: {0}".format( + auth_header)) + response.request.headers['Authorization'] = auth_header # Consume the content so we can reuse the connection for the next # request. @@ -291,6 +302,19 @@ def handle_401(self, response, **kwargs): log.debug("handle_401(): returning {0}".format(response)) return response + def handle_407(self, response, **kwargs): + """Handles 407's, attempts to use gssapi/kerberos authentication""" + + log.debug("handle_407(): Handling: 407") + if _negotiate_value(response) is not None: + _r = self.authenticate_user(response, **kwargs) + log.debug("handle_407(): returning {0}".format(_r)) + return _r + else: + log.debug("handle_407(): Kerberos is not supported") + log.debug("handle_407(): returning {0}".format(response)) + return response + def handle_other(self, response): """Handles all responses with the exception of 401s. @@ -374,6 +398,7 @@ def authenticate_server(self, response): def handle_response(self, response, **kwargs): """Takes the given response and tries kerberos-auth, as needed.""" num_401s = kwargs.pop('num_401s', 0) + num_407s = kwargs.pop('num_407s', 0) # Check if we have already tried to get the CBT data value if not self.cbt_binding_tried and self.send_cbt: @@ -407,6 +432,19 @@ def handle_response(self, response, **kwargs): # Authentication has failed. Return the 401 response. log.debug("handle_response(): returning 401 %s", response) return response + elif response.status_code == 407 and num_407s < 2: + # 407 Unauthorized. Handle it, and if it still comes back as 407, + # that means authentication failed. + _r = self.handle_407(response, **kwargs) + log.debug("handle_response(): returning %s", _r) + log.debug("handle_response() has seen %d 407 responses", num_407s) + num_407s += 1 + return self.handle_response(_r, num_407s=num_407s, **kwargs) + elif response.status_code == 407 and num_407s >= 2: + # Still receiving 407 responses after attempting to handle them. + # Authentication has failed. Return the 407 response. + log.debug("handle_response(): returning 407 %s", response) + return response else: _r = self.handle_other(response) log.debug("handle_response(): returning %s", _r) From 9c53fb693c440ea3317a2f02e285f2fbf81ca0a4 Mon Sep 17 00:00:00 2001 From: Thimo Emmerich Date: Tue, 7 Apr 2020 17:06:36 +0200 Subject: [PATCH 2/2] Added testcases for kerberos based proxy authentication --- tests/test_requests_kerberos.py | 92 +++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/tests/test_requests_kerberos.py b/tests/test_requests_kerberos.py index ebaca37..3c67b1b 100644 --- a/tests/test_requests_kerberos.py +++ b/tests/test_requests_kerberos.py @@ -216,6 +216,52 @@ def test_authenticate_user(self): clientStep_continue.assert_called_with("CTX", "token") clientResponse.assert_called_with("CTX") + def test_authenticate_user2(self): + with patch.multiple(kerberos_module_name, + authGSSClientInit=clientInit_complete, + authGSSClientResponse=clientResponse, + authGSSClientStep=clientStep_continue): + + response_ok = requests.Response() + response_ok.url = "http://not_used.example.org/" + response_ok.status_code = 200 + response_ok.headers = {'proxy-authenticate': 'negotiate servertoken'} + + connection = Mock() + connection.send = Mock(return_value=response_ok) + + raw = Mock() + raw.release_conn = Mock(return_value=None) + + request = requests.Request() + response = requests.Response() + response.request = request + response.url = "http://not_used.example.org/" + response.headers = {'proxy-authenticate': 'negotiate token'} + response.status_code = 407 + response.connection = connection + response._content = "" + response.raw = raw + auth = requests_kerberos.HTTPKerberosAuth() + kwa = {'proxies' : {'http': 'http://www.example.org:10080', 'https': 'https://www.example.org:10080'} } + r = auth.authenticate_user(response, **kwa) + + self.assertTrue(response in r.history) + self.assertEqual(r, response_ok) + self.assertEqual( + request.headers['Proxy-Authorization'], + 'Negotiate GSSRESPONSE') + connection.send.assert_called_with(request, **kwa) + raw.release_conn.assert_called_with() + clientInit_complete.assert_called_with( + "HTTP@www.example.org", + gssflags=( + kerberos.GSS_C_MUTUAL_FLAG | + kerberos.GSS_C_SEQUENCE_FLAG), + principal=None) + clientStep_continue.assert_called_with("CTX", "token") + clientResponse.assert_called_with("CTX") + def test_handle_401(self): with patch.multiple(kerberos_module_name, authGSSClientInit=clientInit_complete, @@ -261,6 +307,52 @@ def test_handle_401(self): clientStep_continue.assert_called_with("CTX", "token") clientResponse.assert_called_with("CTX") + def test_handle_407(self): + with patch.multiple(kerberos_module_name, + authGSSClientInit=clientInit_complete, + authGSSClientResponse=clientResponse, + authGSSClientStep=clientStep_continue): + + response_ok = requests.Response() + response_ok.url = "http://not_used.example.org/" + response_ok.status_code = 200 + response_ok.headers = {'proxy-authenticate': 'negotiate servertoken'} + + connection = Mock() + connection.send = Mock(return_value=response_ok) + + raw = Mock() + raw.release_conn = Mock(return_value=None) + + request = requests.Request() + response = requests.Response() + response.request = request + response.url = "http://not_used.example.org/" + response.headers = {'proxy-authenticate': 'negotiate token'} + response.status_code = 407 + response.connection = connection + response._content = "" + response.raw = raw + auth = requests_kerberos.HTTPKerberosAuth() + kwa = {'proxies' : {'http': 'http://www.example.org:10080', 'https': 'https://www.example.org:10080'} } + r = auth.handle_407(response, **kwa) + + self.assertTrue(response in r.history) + self.assertEqual(r, response_ok) + self.assertEqual( + request.headers['Proxy-Authorization'], + 'Negotiate GSSRESPONSE') + connection.send.assert_called_with(request, **kwa) + raw.release_conn.assert_called_with() + clientInit_complete.assert_called_with( + "HTTP@www.example.org", + gssflags=( + kerberos.GSS_C_MUTUAL_FLAG | + kerberos.GSS_C_SEQUENCE_FLAG), + principal=None) + clientStep_continue.assert_called_with("CTX", "token") + clientResponse.assert_called_with("CTX") + def test_authenticate_server(self): with patch.multiple(kerberos_module_name, authGSSClientStep=clientStep_complete):