Negotiate authentication with Python and PycURL on FreeBSD

I wanted to download data from a webserver that is part of an Active Directory and offers negotiate authentication via Kerberos only, i.e. the webserver returns the HTTP error code 401 and a HTTP header WWW-Authenticate: Negotiate if your client does not provide any authorization. The client is not part of that Active Directory and should not use a system wide keytab nor a delegation user. As a base installation of FreeBSD comes with the Heimdal Kerberos tools, we can use kinit to create a temporary keytab and impersonate the specific Active Directory user who is allowed to download from that webserver.

Steps

  1. If curl is already installed on your system, make sure it has support for GSSAPI:
    # make -C /usr/ports/ftp/curl config
    
    If GSSAPI_NONE is selected, choose GSSAPI_BASE, exit the dialog, save the options, and recompile curl:
    # make -C /usr/ports/ftp/curl all deinstall reinstall clean
    
  2. Now install PycURL, which is the Python wrapper for curl:
    # make -C /usr/ports/ftp/py-pycurl install clean
    
    If curl is not installed on your system, make sure to select GSSAPI_BASE during its options dialog as described above
  3. Your /etc/resolv.conf should reference the AD controllers as nameservers
  4. Create the Kerberos configuration file /etc/krb5.conf:
    [libdefaults]
      dns_lookup_kdc   = yes
      dns_lookup_realm = yes
    
  5. Use this Python script to download data from the webserver, or use it as outline for your own application:
    #!/usr/local/bin/python2.7
    
    import pycurl, StringIO, getpass, sys, subprocess, os, tempfile
    
    # username and url expected as command line arguments
    if len(sys.argv) != 2:
        sys.exit("usage: %s <username> <url>")
    
    username = sys.argv[1]
    url = sys.argv[2]
    
    # don't echo password on the console
    password = getpass.getpass(username + "'s password: ")
    
    # temporary file to save the user's keytab to, which gets deleted
    # when the file is closed
    # both kinit and pycurl honor this environment variable
    os.environ["KRB5CCNAME"] = tempfile.NamedTemporaryFile().name
    
    # create temporary keytab, read password from stdin
    # had no success with lifetime or renew time of 120 seconds or less
    kinit = subprocess.Popen(("kinit", "--password-file=STDIN", "-l", "300",
                              "-r", "300", "--no-forwardable", username),
                             stdin=subprocess.PIPE, stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE, close_fds=True)
    (stdoutdata, stderrdata) = kinit.communicate(password)
    
    # user pycurl to read url into this buffer
    buffer = StringIO.StringIO()
    curl = pycurl.Curl()
    curl.setopt(pycurl.URL, url)
    curl.setopt(pycurl.WRITEDATA, buffer)
    curl.setopt(pycurl.USERNAME, username)
    curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_NEGOTIATE)
    # curl.setopt(pycurl.VERBOSE, True)
    curl.perform()
    
    print "HTTP code: %i" % (curl.getinfo(pycurl.RESPONSE_CODE),)
    print buffer.getvalue()
    
    # after a pycurl object is closed, we can no longer call getinfo() on it
    curl.close()
    
  6. Please note that kinit will ask the user for a new password if the current one has expired.