duosecurity/duo_client_python

Getting 40103 on making GET REST API calls

Nirmalendu opened this issue · 5 comments

The provided Python client works fine and gives expected outcome. However, when I try to build a standalone Python file based on the API documentation I get 40103

Description

I followed the API documentation at https://duo.com/docs/adminapi#api-clients.
I see in the Python client, the connection isn't a simple request.get(uri, header) but involves passing a certificate and specifying the port numbers. Since, there is no separate repo for the REST API, I am sharing my findings here.
The API I am calling is GET /admin/v2/logs/authentication'

Expected Behavior

200 OK { "response": { "authlogs": [ { "access_device": { "browser": "Chrome Mobile", "browser_version": "120.0.0.0", "epkey": null, "flash_version": null, "hostname": null, "ip": "XXXXXXXXXXX, "is_encryption_enabled": "unknown", "is_firewall_enabled": "unknown", "is_password_set": "unknown", "java_version": null, "location": { "city": "XXXXXXXXXXX", "country": "XXXXXXXXXXX", "state": "XXXXXXXXXXX" }, "os": "Android", "os_version": "10" }, "adaptive_trust_assessments": {}, "alias": "unknown", "application": { "key": "XXXXXXXXXXX", "name": "User Portal" }, "auth_device": { "ip": null, "key": "XXXXXXXXXXX", "location": { "city": null, "country": null, "state": null }, "name": "XXXXXXXXXXX" }, "email": null, "event_type": "enrollment", "factor": "Platform authenticator (2fa)", "isotimestamp": "2023-12-16T06:45:29.609614+00:00", "ood_software": null, "reason": null, "result": "success", "timestamp": 1702709129, "trusted_endpoint_status": "unknown", "txid": "00fe2487-be6a-4d2d-905b-bb7aa9956353", "user": { "groups": [], "key": "XXXXXXXXXXXXXX", "name": "XXXXXXXXXXX@gmail.com" } } ], "metadata": { "next_offset": [ "1702709129609", "00fe2487-be6a-4d2d-905b-bb7aa9956353" ], "total_objects": 1 } }, "stat": "OK" }

Actual Behavior

Error: 401 - {"code": 40103, "message": "Invalid signature in request credentials", "stat": "FAIL"}

Steps to Reproduce

  1. Follow the documentation at https://duo.com/docs/adminapi#api-clients to create the authorization header
  2. Do a 'response = requests.get(url=url, headers=headers)'
yizshi commented

https://help.duo.com/s/article/1338?language=en_US
40103 means the signing signature is not correct.
You could follow our doc on how we do authentication for api request https://duo.com/docs/adminapi#authentication
Or if you want to follow our code example, this is where the authentication happens in the client. https://github.com/duosecurity/duo_client_python/blob/master/duo_client/client.py#L113

I followed the doc, also confirmed the signature. I still get the same error. I see that in client.py the request happens after CertValidatingHTTPSConnection. Is passing the certificate mandatory? Consider the case when a user wants to try out the APIs on Postman/Fiddler. How will we do that?

yizshi commented

I see that in client.py the request happens after CertValidatingHTTPSConnection. Is passing the certificate mandatory?

Not necessarily, in the elif block above it you can see an example on how you can make request without pass in any certs

This is the code I am using, please let me know if I followed the steps wrong:

def main():
    #define variables
    ikey='XXXXXX'
    skey='XXXXXX'
    host='api-dd23b954.duosecurity.com'
    path='/admin/v2/logs/authentication'
    method='GET'
    params={}
    params['mintime']='1702615541159'
    params['maxtime']='1702715541159'
    #create headers
    headers = sign(method=method, host=host, path=path, params=params, skey=skey, ikey=ikey)
    #create token
    #add content headers
    #make uri
    url = 'https://' + 'api-dd23b954.duosecurity.com' + '/admin/v2/logs/authentication'
    #make request
    headers['Host'] = host.encode('utf-8')
    #headers['Content-Length']='35'.encode('utf-8')
    #headers['Content-Type']='application/x-www-form-urlencoded'
    response = requests.get(url=url, headers=headers)
    if response.status_code == 200:
    # Print the response content (usually JSON data for APIs)
        print(response.json())
    else:
    # Print an error message if the request was not successful
        print(f"Error: {response.status_code} - {response.text}")

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 = email.utils.formatdate()
    canon = [now, method.upper(), host.lower(), path]
    args = []
    for key in sorted(params.keys()):
        val = params[key].encode("utf-8")
        args.append(
            '%s=%s' % (urllib.parse.
                       quote(key, '~'), urllib.parse.quote(val, '~')))
    canon.append('&'.join(args))
    canon = '\n'.join(canon)

    # sign canonical string
    sig = hmac.new(bytes(skey, encoding='utf-8'),
                   bytes(canon, encoding='utf-8'),
                   hashlib.sha512)
    auth = '%s:%s' % (ikey, sig.hexdigest())

    # return headers
    return {'Date': now, 'Authorization': 'Basic %s' % base64.b64encode(bytes(auth, encoding="utf-8")).decode()}


I have tried with sha1 as mentioned in the docs and sha512 as used in the python client. I keep getting 40103.
Thank you for your help.

@Nirmalendu the query parameters need to be included in the URL:

url = 'https://' + 'api-dd23b954.duosecurity.com' + '/admin/v2/logs/authentication'
url += f'?mintime={params["mintime"]}&maxtime={params["maxtime"]}'

After making that change, your script works for me. That should get it working for you!

For future reference, note that in general contacting Duo Support or posting in the Duo Community Forums are more appropriate avenues for help with application development.