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

Mutual auth updates #16

Merged
merged 2 commits into from
May 17, 2019
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
79 changes: 50 additions & 29 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,57 +32,78 @@ the 401 response.
Mutual Authentication
---------------------

REQUIRED
Mutual authentication is a poorly-named feature of the GSSAPI which doesn't
provide any additional security benefit to most possible uses of
requests_gssapi. Practically speaking, in most mechanism implementations
(including krb5), it requires another round-trip between the client and server
during the authentication handshake. Many clients and servers do not properly
handle the authentication handshake taking more than one round-trip. If you
encounter a MutualAuthenticationError, this is probably why.

So long as you're running over a TLS link whose security guarantees you trust,
there's no benefit to mutual authentication. If you don't trust the link at
all, mutual authentication won't help (since it's not tamper-proof, and GSSAPI
isn't being used post-authentication. There's some middle ground between the
two where it helps a small amount (e.g., passive adversary over
encrypted-but-unverified channel), but for Negotiate (what we're doing here),
it's not generally helpful.

For a more technical explanation of what mutual authentication actually
guarantees, I refer you to rfc2743 (GSSAPIv2), rfc4120 (krb5 in GSSAPI),
rfc4178 (SPNEGO), and rfc4559 (HTTP Negotiate).


DISABLED
^^^^^^^^

By default, ``HTTPSPNEGOAuth`` will require mutual authentication from the
server, and if a server emits a non-error response which cannot be
authenticated, a ``requests_gssapi.errors.MutualAuthenticationError`` will
be raised. If a server emits an error which cannot be authenticated, it will
be returned to the user but with its contents and headers stripped. If the
response content is more important than the need for mutual auth on errors,
(eg, for certain WinRM calls) the stripping behavior can be suppressed by
setting ``sanitize_mutual_error_response=False``:
By default, there's no need to explicitly disable mutual authentication.
However, for compatability with older versions of request_gssapi or
requests_kerberos, you can explicitly request it not be attempted:

.. code-block:: python

>>> import requests
>>> from requests_gssapi import HTTPSPNEGOAuth, REQUIRED
>>> gssapi_auth = HTTPSPNEGOAuth(mutual_authentication=REQUIRED, sanitize_mutual_error_response=False)
>>> r = requests.get("https://windows.example.org/wsman", auth=gssapi_auth)
>>> from requests_gssapi import HTTPSPNEGOAuth, DISABLED
>>> gssapi_auth = HTTPSPNEGOAuth(mutual_authentication=DISABLED)
>>> r = requests.get("https://example.org", auth=gssapi_auth)
...


OPTIONAL
REQUIRED
^^^^^^^^

If you'd prefer to not require mutual authentication, you can set your
preference when constructing your ``HTTPSPNEGOAuth`` object:
This was historically the default, but no longer is. If requested,
``HTTPSPNEGOAuth`` will require mutual authentication from the server, and if
a server emits a non-error response which cannot be authenticated, a
``requests_gssapi.errors.MutualAuthenticationError`` will be raised. (See
above for what this means.) If a server emits an error which cannot be
authenticated, it will be returned to the user but with its contents and
headers stripped. If the response content is more important than the need for
mutual auth on errors, (eg, for certain WinRM calls) the stripping behavior
can be suppressed by setting ``sanitize_mutual_error_response=False``:

.. code-block:: python

>>> import requests
>>> from requests_gssapi import HTTPSPNEGOAuth, OPTIONAL
>>> gssapi_auth = HTTPSPNEGOAuth(mutual_authentication=OPTIONAL)
>>> r = requests.get("http://example.org", auth=gssapi_auth)
>>> from requests_gssapi import HTTPSPNEGOAuth, REQUIRED
>>> gssapi_auth = HTTPSPNEGOAuth(mutual_authentication=REQUIRED, sanitize_mutual_error_response=False)
>>> r = requests.get("https://windows.example.org/wsman", auth=gssapi_auth)
...

This will cause ``requests_gssapi`` to attempt mutual authentication if the
server advertises that it supports it, and cause a failure if authentication
fails, but not if the server does not support it at all.

DISABLED
OPTIONAL
^^^^^^^^

While we don't recommend it, if you'd prefer to never attempt mutual
authentication, you can do that as well:
This will cause ``requests_gssapi`` to attempt mutual authentication if the
server advertises that it supports it, and cause a failure if authentication
fails, but not if the server does not support it at all. This is probably not
what you want: link tampering will either cause hard failures, or silently
cause it to not happen at all. It is retained for compatability.

.. code-block:: python

>>> import requests
>>> from requests_gssapi import HTTPSPNEGOAuth, DISABLED
>>> gssapi_auth = HTTPSPNEGOAuth(mutual_authentication=DISABLED)
>>> r = requests.get("http://example.org", auth=gssapi_auth)
>>> from requests_gssapi import HTTPSPNEGOAuth, OPTIONAL
>>> gssapi_auth = HTTPSPNEGOAuth(mutual_authentication=OPTIONAL)
>>> r = requests.get("https://example.org", auth=gssapi_auth)
...

Opportunistic Authentication
Expand Down
4 changes: 2 additions & 2 deletions requests_gssapi/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import gssapi

from .gssapi_ import REQUIRED, HTTPSPNEGOAuth, SPNEGOExchangeError, log
from .gssapi_ import DISABLED, HTTPSPNEGOAuth, SPNEGOExchangeError, log

# python 2.7 introduced a NullHandler which we want to use, but to support
# older versions, we implement our own if needed.
Expand All @@ -21,7 +21,7 @@ def emit(self, record):

class HTTPKerberosAuth(HTTPSPNEGOAuth):
"""Deprecated compat shim; see HTTPSPNEGOAuth instead."""
def __init__(self, mutual_authentication=REQUIRED, service="HTTP",
def __init__(self, mutual_authentication=DISABLED, service="HTTP",
delegate=False, force_preemptive=False, principal=None,
hostname_override=None, sanitize_mutual_error_response=True):
# put these here for later
Expand Down
12 changes: 7 additions & 5 deletions requests_gssapi/gssapi_.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ class HTTPSPNEGOAuth(AuthBase):
"""Attaches HTTP GSSAPI Authentication to the given Request object.

`mutual_authentication` controls whether GSSAPI should attempt mutual
authentication. It may be `REQUIRED` (default), `OPTIONAL`, or
`DISABLED`.
authentication. It may be `REQUIRED`, `OPTIONAL`, or `DISABLED`
(default).

`target_name` specifies the remote principal name. It may be either a
GSSAPI name type or a string (default: "HTTP" at the DNS host).
Expand All @@ -101,8 +101,9 @@ class HTTPSPNEGOAuth(AuthBase):

`sanitize_mutual_error_response` controls whether we should clean up
server responses. See the `SanitizedResponse` class.

"""
def __init__(self, mutual_authentication=REQUIRED, target_name="HTTP",
def __init__(self, mutual_authentication=DISABLED, target_name="HTTP",
delegate=False, opportunistic_auth=False, creds=None,
sanitize_mutual_error_response=True):
self.context = {}
Expand All @@ -123,10 +124,11 @@ def generate_request_header(self, response, host, is_preemptive=False):

"""

gssflags = [gssapi.RequirementFlag.mutual_authentication,
gssapi.RequirementFlag.out_of_sequence_detection]
gssflags = [gssapi.RequirementFlag.out_of_sequence_detection]
if self.delegate:
gssflags.append(gssapi.RequirementFlag.delegate_to_peer)
if self.mutual_authentication != DISABLED:
gssflags.append(gssapi.RequirementFlag.mutual_authentication)

try:
gss_stage = "initiating context"
Expand Down
24 changes: 16 additions & 8 deletions test_requests_gssapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import requests_gssapi
import unittest

from requests_gssapi import REQUIRED

# Note: we're not using the @mock.patch decorator:
# > My only word of warning is that in the past, the patch decorator hides
# > tests when using the standard unittest library.
Expand All @@ -26,8 +28,8 @@
# construction, so construct a *really* fake one
fail_resp = Mock(side_effect=gssapi.exceptions.GSSError(0, 0))

gssflags = [gssapi.RequirementFlag.mutual_authentication,
gssapi.RequirementFlag.out_of_sequence_detection]
gssflags = [gssapi.RequirementFlag.out_of_sequence_detection]
mutflags = gssflags + [gssapi.RequirementFlag.mutual_authentication]
gssdelegflags = gssflags + [gssapi.RequirementFlag.delegate_to_peer]

# The base64 behavior we want is that encoding produces a string, but decoding
Expand Down Expand Up @@ -237,7 +239,8 @@ def test_handle_other(self):
'www-authenticate': b64_negotiate_server,
'authorization': b64_negotiate_response}

auth = requests_gssapi.HTTPKerberosAuth()
auth = requests_gssapi.HTTPKerberosAuth(
mutual_authentication=REQUIRED)
auth.context = {"www.example.org": gssapi.SecurityContext}

r = auth.handle_other(response_ok)
Expand All @@ -255,7 +258,8 @@ def test_handle_response_200(self):
'www-authenticate': b64_negotiate_server,
'authorization': b64_negotiate_response}

auth = requests_gssapi.HTTPKerberosAuth()
auth = requests_gssapi.HTTPKerberosAuth(
mutual_authentication=REQUIRED)
auth.context = {"www.example.org": gssapi.SecurityContext}

r = auth.handle_response(response_ok)
Expand All @@ -272,7 +276,8 @@ def test_handle_response_200_mutual_auth_required_failure(self):
response_ok.status_code = 200
response_ok.headers = {}

auth = requests_gssapi.HTTPKerberosAuth()
auth = requests_gssapi.HTTPKerberosAuth(
mutual_authentication=REQUIRED)
auth.context = {"www.example.org": "CTX"}

self.assertRaises(requests_gssapi.MutualAuthenticationError,
Expand All @@ -290,7 +295,8 @@ def test_handle_response_200_mutual_auth_required_failure_2(self):
'www-authenticate': b64_negotiate_server,
'authorization': b64_negotiate_response}

auth = requests_gssapi.HTTPKerberosAuth()
auth = requests_gssapi.HTTPKerberosAuth(
mutual_authentication=REQUIRED)
auth.context = {"www.example.org": gssapi.SecurityContext}

self.assertRaises(requests_gssapi.MutualAuthenticationError,
Expand Down Expand Up @@ -348,7 +354,8 @@ def test_handle_response_500_mutual_auth_required_failure(self):
response_500.raw = "RAW"
response_500.cookies = "COOKIES"

auth = requests_gssapi.HTTPKerberosAuth()
auth = requests_gssapi.HTTPKerberosAuth(
mutual_authentication=REQUIRED)
auth.context = {"www.example.org": "CTX"}

r = auth.handle_response(response_500)
Expand Down Expand Up @@ -524,7 +531,8 @@ def test_delegation(self):
response.connection = connection
response._content = ""
response.raw = raw
auth = requests_gssapi.HTTPKerberosAuth(1, "HTTP", True)
auth = requests_gssapi.HTTPKerberosAuth(service="HTTP",
delegate=True)
r = auth.authenticate_user(response)

self.assertTrue(response in r.history)
Expand Down