Authorization signature for API requests

I am trying to use the Admin API.
I’m being told my Authorization signature is invalid with a HTTP 401 response code (“Invalid signature in request credentials” - 40103 to be precise).

The trouble is I’m following the guidance in the documentation here: Duo Admin API | Duo Security
… including using the same signing function, and the same sample data - but not getting the same signature back!

Is there anyone out there who’s got this working, and who can advise what I’m doing wrong?

Here’s my sample Python code - this doesn’t touch the API, but just calculates the Authorization signature based on the values in the example:

######################################################

import base64
from email import utils
import hmac
import hashlib
import urllib
import logging
import sys
import os

log = logging.getLogger(__name__)
out_hdlr = logging.StreamHandler(sys.stdout)
out_hdlr.setFormatter(logging.Formatter('%(asctime)s ' + os.path.basename(__file__) + ': %(message)s'))
out_hdlr.setLevel(logging.INFO)
log.addHandler(out_hdlr)
log.setLevel(logging.INFO)

def sign(method, host, path, params, skey, ikey):
        """
        Return HTTP Basic Authentication ("Authorization" and "Date") headers.
        method, host, path: strings from request
        params: dict of request parameters
        skey: secret key
        ikey: integration key
        """

        # create canonical string
        #now = utils.formatdate()
        now = "Tue, 21 Aug 2012 17:29:18 -0000"
        log.debug(now)
        canon = [now, method.upper(), host.lower(), path]
        args = []
        for key in sorted(params.keys()):
                val = params[key]
                if isinstance(val, unicode):
                        val = val.encode("utf-8")
                args.append('%s=%s' % (urllib.quote(key, '~'), urllib.quote(val, '~')))
        canon.append('&'.join(args))
        canon = '\n'.join(canon)

        log.info("Outputting canonical form of data to be signed")
        log.info(canon)

        # sign canonical string
        sig = hmac.new(skey, canon, hashlib.sha1)
        auth = '%s:%s' % (ikey, sig.hexdigest())

        # return headers
        return {'Date': now, 'Authorization': 'Basic %s' % base64.b64encode(auth)}

log.info("Starting Duo log retrieval")

params={"realname": "First Last",
        "username": "root"}

headers=sign(method="POST",
        host="api-xxxxxxxx.duosecurity.com",
        path="/admin/v1/users",
        params=params,
        skey="Zh5eGmUq9zpfQnyUIu5OL9iWoMMv5ZNmk3zLJ4Ep",
        ikey="DIWJ8X6AEYOR5OMC6TQ1")

log.info("Outputting headers")
log.info(headers)

#Gives output like:
#
# Starting Duo log retrieval
#
# Outputting canonical form of data to be signed
# Tue, 21 Aug 2012 17:29:18 -0000
# POST
# ■■■■ <- the correct host goes here, this has been obfuscated by Duo's Community forum
# /admin/v1/users
# realname=First%20Last&username=root
# 
# Outputting headers
# {'Date': 'Tue, 21 Aug 2012 17:29:18 -0000', 'Authorization': 'Basic RElXSjhYNkFFWU9SNU9NQzZUUTE6YzFlZjQzNzY3YzNlYjNiMzI1OGRiZGRjYTZmOGQwOTQxZTA4NWI5Mg=='}

# THIS DOES NOT MATCH ONLINE EXAMPLE at https://duo.com/docs/adminapi#authentication
# Apparently Authorization should be RElXSjhYNkFFWU9SNU9NQzZUUTE6MmQ5N2Q2MTY2MzE5NzgxYjVhM2EwN2FmMzlkMzY2ZjQ5MTIzNGVkYw==

######################################################

That signature example value is not right. We will get that fixed. Go ahead and actually test your code against Duo, ignoring the example value.

Thanks for the response Kristina.

That’s unfortunate regarding the example being wrong - is my value correct then?

Because if its the sample code (“sign” function) that is incorrect then this would explain why this isn’t working for me in my production instance also…

So is the example on Auth API | Duo Security correct now?