try: import kerberos except ImportError: import kerberos_sspi as kerberos import re import logging from requests.auth import AuthBase from requests.models import Response from requests.compat import urlparse, StringIO from requests.structures import CaseInsensitiveDict from requests.cookies import cookiejar_from_dict from .exceptions import MutualAuthenticationError, KerberosExchangeError log = logging.getLogger(__name__) # Different types of mutual authentication: # with mutual_authentication set to REQUIRED, all responses will be # authenticated with the exception of errors. Errors will have their contents # and headers stripped. If a non-error response cannot be authenticated, a # MutualAuthenticationError exception will be raised. # with mutual_authentication set to OPTIONAL, mutual authentication will be # attempted if supported, and if supported and failed, a # MutualAuthenticationError exception will be raised. Responses which do not # support mutual authentication will be returned directly to the user. # with mutual_authentication set to DISABLED, mutual authentication will not be # attempted, even if supported. REQUIRED = 1 OPTIONAL = 2 DISABLED = 3 class SanitizedResponse(Response): """The :class:`Response ` object, which contains a server's response to an HTTP request. This differs from `requests.models.Response` in that it's headers and content have been sanitized. This is only used for HTTP Error messages which do not support mutual authentication when mutual authentication is required.""" def __init__(self, response): super(SanitizedResponse, self).__init__() self.status_code = response.status_code self.encoding = response.encoding self.raw = response.raw self.reason = response.reason self.url = response.url self.request = response.request self.connection = response.connection self._content_consumed = True self._content = "" self.cookies = cookiejar_from_dict({}) self.headers = CaseInsensitiveDict() self.headers['content-length'] = '0' for header in ('date', 'server'): if header in response.headers: self.headers[header] = response.headers[header] def _negotiate_value(response): """Extracts the gssapi authentication token from the appropriate header""" if hasattr(_negotiate_value, 'regex'): regex = _negotiate_value.regex else: # There's no need to re-compile this EVERY time it is called. Compile # it once and you won't have the performance hit of the compilation. regex = re.compile('(?:.*,)*\s*Negotiate\s*([^,]*),?', re.I) _negotiate_value.regex = regex authreq = response.headers.get('www-authenticate', None) if authreq: match_obj = regex.search(authreq) if match_obj: return match_obj.group(1) return None class HTTPKerberosAuth(AuthBase): """Attaches HTTP GSSAPI/Kerberos Authentication to the given Request object.""" def __init__( self, mutual_authentication=REQUIRED, service="HTTP", delegate=False, force_preemptive=False, principal=None, hostname_override=None): self.context = {} self.mutual_authentication = mutual_authentication self.delegate = delegate self.pos = None self.service = service self.force_preemptive = force_preemptive self.principal = principal self.hostname_override = hostname_override def generate_request_header(self, response, host, is_preemptive=False): """ Generates the GSSAPI authentication token with kerberos. If any GSSAPI step fails, raise KerberosExchangeError with failure detail. """ # Flags used by kerberos module. gssflags = kerberos.GSS_C_MUTUAL_FLAG | kerberos.GSS_C_SEQUENCE_FLAG if self.delegate: gssflags |= kerberos.GSS_C_DELEG_FLAG try: kerb_stage = "authGSSClientInit()" # contexts still need to be stored by host, but hostname_override # allows use of an arbitrary hostname for the kerberos exchange # (eg, in cases of aliased hosts, internal vs external, CNAMEs # w/ name-based HTTP hosting) kerb_host = self.hostname_override if self.hostname_override is not None else host kerb_spn = "{0}@{1}".format(self.service, kerb_host) result, self.context[host] = kerberos.authGSSClientInit(kerb_spn, gssflags=gssflags, principal=self.principal) if result < 1: raise EnvironmentError(result, kerb_stage) # if we have a previous response from the server, use it to continue # the auth process, otherwise use an empty value negotiate_resp_value = '' if is_preemptive else _negotiate_value(response) kerb_stage = "authGSSClientStep()" result = kerberos.authGSSClientStep(self.context[host], negotiate_resp_value) if result < 0: raise EnvironmentError(result, kerb_stage) kerb_stage = "authGSSClientResponse()" gss_response = kerberos.authGSSClientResponse(self.context[host]) return "Negotiate {0}".format(gss_response) except kerberos.GSSError as error: log.exception( "generate_request_header(): {0} failed:".format(kerb_stage)) log.exception(error) raise KerberosExchangeError("%s failed: %s" % (kerb_stage, str(error.args))) except EnvironmentError as error: # ensure we raised this for translation to KerberosExchangeError # by comparing errno to result, re-raise if not if error.errno != result: raise message = "{0} failed, result: {1}".format(kerb_stage, result) log.error("generate_request_header(): {0}".format(message)) raise KerberosExchangeError(message) def authenticate_user(self, response, **kwargs): """Handles user authentication with gssapi/kerberos""" host = urlparse(response.url).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 # Consume the content so we can reuse the connection for the next # request. response.content response.raw.release_conn() _r = response.connection.send(response.request, **kwargs) _r.history.append(response) log.debug("authenticate_user(): returning {0}".format(_r)) return _r def handle_401(self, response, **kwargs): """Handles 401's, attempts to use gssapi/kerberos authentication""" log.debug("handle_401(): Handling: 401") if _negotiate_value(response) is not None: _r = self.authenticate_user(response, **kwargs) log.debug("handle_401(): returning {0}".format(_r)) return _r else: log.debug("handle_401(): Kerberos is not supported") log.debug("handle_401(): returning {0}".format(response)) return response def handle_other(self, response): """Handles all responses with the exception of 401s. This is necessary so that we can authenticate responses if requested""" log.debug("handle_other(): Handling: %d" % response.status_code) if self.mutual_authentication in (REQUIRED, OPTIONAL): is_http_error = response.status_code >= 400 if _negotiate_value(response) is not None: log.debug("handle_other(): Authenticating the server") if not self.authenticate_server(response): # Mutual authentication failure when mutual auth is wanted, # raise an exception so the user doesn't use an untrusted # response. log.error("handle_other(): Mutual authentication failed") raise MutualAuthenticationError("Unable to authenticate " "{0}".format(response)) # Authentication successful log.debug("handle_other(): returning {0}".format(response)) return response elif is_http_error or self.mutual_authentication == OPTIONAL: if not response.ok: log.error("handle_other(): Mutual authentication unavailable " "on {0} response".format(response.status_code)) if self.mutual_authentication == REQUIRED: return SanitizedResponse(response) else: return response else: # Unable to attempt mutual authentication when mutual auth is # required, raise an exception so the user doesnt use an # untrusted response. log.error("handle_other(): Mutual authentication failed") raise MutualAuthenticationError("Unable to authenticate " "{0}".format(response)) else: log.debug("handle_other(): returning {0}".format(response)) return response def authenticate_server(self, response): """ Uses GSSAPI to authenticate the server. Returns True on success, False on failure. """ log.debug("authenticate_server(): Authenticate header: {0}".format( _negotiate_value(response))) host = urlparse(response.url).hostname try: result = kerberos.authGSSClientStep(self.context[host], _negotiate_value(response)) except kerberos.GSSError: log.exception("authenticate_server(): authGSSClientStep() failed:") return False if result < 1: log.error("authenticate_server(): authGSSClientStep() failed: " "{0}".format(result)) return False log.debug("authenticate_server(): returning {0}".format(response)) return True def handle_response(self, response, **kwargs): """Takes the given response and tries kerberos-auth, as needed.""" num_401s = kwargs.pop('num_401s', 0) if self.pos is not None: # Rewind the file position indicator of the body to where # it was to resend the request. response.request.body.seek(self.pos) if response.status_code == 401 and num_401s < 2: # 401 Unauthorized. Handle it, and if it still comes back as 401, # that means authentication failed. _r = self.handle_401(response, **kwargs) log.debug("handle_response(): returning %s", _r) log.debug("handle_response() has seen %d 401 responses", num_401s) num_401s += 1 return self.handle_response(_r, num_401s=num_401s, **kwargs) elif response.status_code == 401 and num_401s >= 2: # Still receiving 401 responses after attempting to handle them. # Authentication has failed. Return the 401 response. log.debug("handle_response(): returning 401 %s", response) return response else: _r = self.handle_other(response) log.debug("handle_response(): returning %s", _r) return _r def deregister(self, response): """Deregisters the response handler""" response.request.deregister_hook('response', self.handle_response) def __call__(self, request): if self.force_preemptive: # add Authorization header before we receive a 401 # by the 401 handler host = urlparse(request.url).hostname auth_header = self.generate_request_header(None, host, is_preemptive=True) log.debug("HTTPKerberosAuth: Preemptive Authorization header: {0}".format(auth_header)) request.headers['Authorization'] = auth_header request.register_hook('response', self.handle_response) try: self.pos = request.body.tell() except AttributeError: # In the case of HTTPKerberosAuth being reused and the body # of the previous request was a file-like object, pos has # the file position of the previous body. Ensure it's set to # None. self.pos = None return request