Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Kerberos proxy authentication handling #149

Merged
merged 3 commits into from
Nov 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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