I'm currently integrating Kerberos authentication support into a custom Pulp client and have completely failed to find any good documentation on how to use the kerberos module.
I managed to find a basic example, which makes reference to "another example in the python-kerberos package", which I assume is a reference to the final test case in the package. I also looked at the XML-RPC wrapper implemented in kobo.
Rather than just documenting this for my own use, I decided to write up and publish what I figured out. Besides, it gives me an excuse to try out Kenneth Reitz's famous requests module :)
Note
After I originally wrote this article, Kenneth accepted a pull request
that added Kerberos authentication support directly to requests
. With
the refactored 1.0 release, that support has been moved out to a separate
requests-kerberos project.
All examples in this document are from a Python 2 interactive session.
When setting up Kerberos authentication on a server, there are two basic modes of operation. The simplest from a client implementation point of view just uses Basic Auth to pass a username and password to the server, which then checks them with the Kerberos realm. That's not the case I'm interested in, since it just looks like ordinary Basic Auth from the client side.
The case I am interested in is the one where the client has a preexisting Kerberos ticket and we want to pass that to the server automatically without the user needing to reenter their password. The relevant HTTP authorization protocol is called "Negotiate".
The basic flow of a typical Kerberos authentication is as follows:
- Client sends an unauthenticated request to the server
- Server sends back a 401 response with a
WWW-Authenticate: Negotiate
header with no authentication details - Client sends a new request with an
Authorization: Negotiate
header - Server checks the
Authorization
header against the Kerberos infrastructure and either allows or denies access accordingly. If access is allowed, it should include aWWW-Authenticate: Negotiate
header with authentication details in the reply. - Client checks the authentication details in the reply to ensure that the request came from the server
This article doesn't cover server side authentication, as I just use
mod_auth_kerb to handle that side of things and set up the application to
accept the REMOTE_USER
setting from Apache. One useful undocumented trick
that mod_auth_kerb
supports is the KrbLocalUserMapping option (which
strips the realm details from the value stored in REMOTE_USER
).
From a client point of view, the kerberos module handles two tasks:
- Figuring out the value to send in the
Authorization
field- Checking Kerberos level authentication of the response provided by the server
The kerberos module does this by exposing the GSS API - this is an ugly interface, but it does work.
This part doesn't involve the kerberos module at all, just a basic HTTP request:
>>> import requests >>> r = requests.get("https://krbhost.example.com/krb") >>> r.status_code 401 >>> r.headers["www-authenticate"] 'Negotiate, Basic realm="Example Realm"'
This example uses a fictional host and realm. This fictional host accepts either Negotiate (i.e. Kerberos tickets) or direct username/password authentication.
As the same header occurs multiple times in the response, requests
reports
it as a comma separated list. This isn't very convenient, so we'll write a
helper to split out the auth headers more cleanly:
>>> def www_auth(response): ... auth_fields = {} ... for field in response.headers.get("www-authenticate", "").split(","): ... kind, __, details = field.strip().partition(" ") ... auth_fields[kind.lower()] = details.strip() ... return auth_fields ... >>> www_auth(r) {'negotiate': '', 'basic': 'realm="Example Realm"'}
That means we can now easily detect when the client should reply with a
Kerberos authenticated connection. For example, a host may provide
two entry points, one configured to use mod_auth_kerb
for
preauthentication of users, while the other handles authentication
entirely at the application level:
>>> r = requests.get("https://krbhost.example.com/krb/") >>> r.status_code == 401 and www_auth(r).get('negotiate') == '' True >>> r = requests.get("https://krbhost.example.com/api/") >>> r.status_code == 401 and www_auth(r).get('negotiate') == '' False
If we accessed the "https://krbhost.example.com/krb/"
URL with a
web browser, it would forward the Kerberos ticket if available (and the
browser is configured to do so), otherwise it would pop up a password
dialog, using the realm info from the WWW-Authenticate: Basic
header as the dialog title (at least, that's what Firefox does -
I assume other browsers are similar)
Now we know we want to send a Kerberos authenticated request to the server,
the kerberos
module comes into play. While this is a very thin wrapper
around a C API, it does at least turn failures into exceptions (rather
than setting the return code) so we'll ignore that value:
>>> __, krb_context = kerberos.authGSSClientInit("HTTP@krbhost.example.com") >>> kerberos.authGSSClientStep(krb_context, "") 0 >>> negotiate_details = kerberos.authGSSClientResponse(krb_context) >>> headers = {"Authorization": "Negotiate " + negotiate_details} >>> r = requests.get("https://krbhost.example.com/krb/", headers=headers) >>> r.status_code 200 >>> r.json ["example_data"]
You can set additional GSS flags in the call to authGSSClientInit
but
I haven't found any need to for simple client authentication via Kerberos.
While we can just trust SSL to ensure the integrity of the response from the server, we can also complete the Kerberos handshake and use it to further authenticate the reply from the server:
>>> kerberos.authGSSClientStep(krb_context, www_auth(r)["negotiate"]) 1 >>> kerberos.authGSSClientClean(krb_context) 1
As with other calls, these should throw an exception if they fail, so even though the return code is passed through from C, it should never be anything other than 1 at the Python level.
Here's a simple class that can help make this a bit easier to use in a client without making any assumptions about the HTTP interface being used:
class KerberosTicket: def __init__(self, service): __, krb_context = kerberos.authGSSClientInit(service) kerberos.authGSSClientStep(krb_context, "") self._krb_context = krb_context self.auth_header = ("Negotiate " + kerberos.authGSSClientResponse(krb_context)) def verify_response(self, auth_header): # Handle comma-separated lists of authentication fields for field in auth_header.split(","): kind, __, details = field.strip().partition(" ") if kind.lower() == "negotiate": auth_details = details.strip() break else: raise ValueError("Negotiate not found in %s" % auth_header) # Finish the Kerberos handshake krb_context = self._krb_context if krb_context is None: raise RuntimeError("Ticket already used for verification") self._krb_context = None kerberos.authGSSClientStep(krb_context, auth_details) kerberos.authGSSClientClean(krb_context)
And an example of using it with requests
:
>>> krb = KerberosTicket("HTTP@krbhost.example.com") >>> headers = {"Authorization": krb.auth_header} >>> r = requests.get("https://krbhost.example.com/krb/", headers=headers) >>> r.status_code 200 >>> krb.verify_response(r.headers["www-authenticate"]) >>>