Skip to content

Commit

Permalink
Add Kerberos proxy authentication handling (#149)
Browse files Browse the repository at this point in the history
* Added Kerberos proxy authentication based on code published here: #148

* Added testcases for kerberos based proxy authentication
  • Loading branch information
enzolis authored Nov 29, 2021
1 parent e7d97fc commit 9fb9414
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 4 deletions.
46 changes: 42 additions & 4 deletions requests_kerberos/kerberos_.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@ def _negotiate_value(response):
regex = re.compile(r'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)
Expand Down Expand Up @@ -235,16 +238,24 @@ 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)
except KerberosExchangeError:
# 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.
Expand All @@ -270,6 +281,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.
Expand Down Expand Up @@ -341,6 +365,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:
Expand Down Expand Up @@ -372,6 +397,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)
Expand Down
95 changes: 95 additions & 0 deletions tests/test_requests_kerberos.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,53 @@ def test_authenticate_user(mock_client, mocker):
}


def test_authenticate_user2(mock_client, mocker):
response_ok = requests.Response()
response_ok.url = "http://www.example.org/"
response_ok.status_code = 200
response_ok.headers = {'proxy-authenticate': 'negotiate c2VydmVydG9rZW4='}

connection = mocker.MagicMock()
connection.send.return_value = response_ok

request = requests.Request()
response = requests.Response()
response.request = request
response.url = "http://www.example.org/"
response.headers = {'proxy-authenticate': 'negotiate dG9rZW4='}
response.status_code = 407
response.connection = connection
response._content = ""
response.raw = mocker.MagicMock(return_value=None)
auth = requests_kerberos.HTTPKerberosAuth()
kwa = {'proxies' : {'http': 'http://www.example.org:10080', 'https': 'https://www.example.org:10080'} }
r = auth.authenticate_user(response, **kwa)

assert response in r.history
assert r == response_ok
assert request.headers["Proxy-Authorization"] == "Negotiate R1NTUkVTUE9OU0U="

assert connection.send.call_count == 1
assert connection.send.call_args[0] == (request,)
assert connection.send.call_args[1] == kwa
assert response.raw.release_conn.call_count == 1
assert response.raw.release_conn.call_args[0] == ()

assert mock_client.call_count == 1
assert mock_client.call_args[1] == {
"username": None,
"hostname": "www.example.org",
"service": "HTTP",
"channel_bindings": None,
"context_req": spnego.ContextReq.sequence_detect | spnego.ContextReq.mutual_auth,
"protocol": "kerberos",
}

assert mock_client.return_value.step.call_count == 1
assert mock_client.return_value.step.call_args[1] == {
"in_token": b"token",
}

def test_handle_401(mock_client, mocker):
response_ok = requests.Response()
response_ok.url = "http://www.example.org/"
Expand Down Expand Up @@ -228,6 +275,54 @@ def test_handle_401(mock_client, mocker):
}


def test_handle_407(mock_client, mocker):
response_ok = requests.Response()
response_ok.url = "http://www.example.org/"
response_ok.status_code = 200
response_ok.headers = {'proxy-authenticate': 'negotiate c2VydmVydG9rZW4='}

connection = mocker.MagicMock()
connection.send.return_value = response_ok

request = requests.Request()
response = requests.Response()
response.request = request
response.url = "http://www.example.org/"
response.headers = {'proxy-authenticate': 'negotiate dG9rZW4='}
response.status_code = 407
response.connection = connection
response._content = ""
response.raw = mocker.MagicMock(return_value=None)
auth = requests_kerberos.HTTPKerberosAuth()
kwa = {'proxies' : {'http': 'http://www.example.org:10080', 'https': 'https://www.example.org:10080'} }
r = auth.handle_407(response, **kwa)

assert response in r.history
assert r == response_ok
assert request.headers["Proxy-Authorization"] == "Negotiate R1NTUkVTUE9OU0U="

assert connection.send.call_count == 1
assert connection.send.call_args[0] == (request,)
assert connection.send.call_args[1] == kwa
assert response.raw.release_conn.call_count == 1
assert response.raw.release_conn.call_args[0] == ()

assert mock_client.call_count == 1
assert mock_client.call_args[1] == {
"username": None,
"hostname": "www.example.org",
"service": "HTTP",
"channel_bindings": None,
"context_req": spnego.ContextReq.sequence_detect | spnego.ContextReq.mutual_auth,
"protocol": "kerberos",
}

assert mock_client.return_value.step.call_count == 1
assert mock_client.return_value.step.call_args[1] == {
"in_token": b"token",
}


def test_authenticate_server(mock_client):
response_ok = requests.Response()
response_ok.url = "http://www.example.org/"
Expand Down

0 comments on commit 9fb9414

Please sign in to comment.