Duo Admin API - admin/v2/logs/authentication


#1

Has anyone use the V2 version of the Logs REST request: i.e. /admin/v2/logs/authentication

The documentation around how to format the next_offset for this request is not detailed enough and there are no examples.


#2

Sorry about that. The paging for the auth logs is a bit different than the paging for other endpoints (described in the Response Paging section).

The first GET on the authentication logs will give you next_offset information with two values, a timestamp and a log event transaction id (txid in the event output).

At the next API call, pass in both the timestamp and ID information as next_offset.

Here are examples using the Duo Python API client:

My first authlogs request; I ask for five records:

kristina:duo_client_python kristina$ python -m duo_client.client --ikey $IKEY --skey $SKEY --host $HOST --path /admin/v2/logs/authentication mintime=1516045230000 maxtime=1547563184847 limit=5 --method GET
200 OK
{
    "response": {
        "authlogs": [
            {blah blah five records of info
                }
            }
        ], 
        "metadata": {
            "next_offset": [
                "1547486297000", 
                "5bea5c1e-682c-4f1d-b3f0-75fd31385bd5"
            ], 
            "total_objects": 2233
        }
    }, 
    "stat": "OK"

The next_offset values are 1547486297000 (timestamp) and 5bea5c1e-682c-4f1d-b3f0-75fd31385bd5 (txid). So, I pass BOTH of those into my request for the next five records:

kristina:duo_client_python kristina$ python -m duo_client.client --ikey $IKEY --skey $SKEY --host $HOST --path /admin/v2/logs/authentication mintime=1516045230000 maxtime=1547563184847 limit=5 next_offset=1547486297000 next_offset=5bea5c1e-682c-4f1d-b3f0-75fd31385bd5 --method GET
200 OK
{
    "response": {
        "authlogs": [
            {blah blah blah five more records
                }
            }
        ], 
        "metadata": {
            "next_offset": [
                "1547475232000", 
                "7854c48b-ae46-45dd-a933-0c02699a9db7"
            ], 
            "total_objects": 2233
        }
    }, 
    "stat": "OK"

That response has new next_offset values, which I pass into the next call, rinse, repeat.

Hope that helps!


#3

Thanks for the info - the documentation for this request has “allow multiple” set to No. I would suggest reviewing this documentation for accuracy and adding a sample request.


#4

I wish we were using the Python library - unfortunately I’m locked into using the Ruby client. From what I see the Ruby client does not handle duplicate parameters since it expects the params to be a hash object.


#5

To clarify, you mean the Duo API Ruby client from https://github.com/duosecurity/duo_api_ruby?


#6

Yes - this is what we are using in our solution.


#7

I have modified my local copy and here is what I get now - on the second request - invalid signature. I have modified the logging so you can see the exact URI along with what is created as the “canon”

Here are the logs from the request. Now to be fair, the first request gives me 2 records and says that there are only 2 records - so perhaps I am supposed to stop at this point? The documentation indicates that we should continue calling for more records as long as the metadata has next_offset values - so that is what I am doing.

First Request - success

Jan 16 19:22:55 cyclops-9218 cloudlogshipper[26859]: esduo: https://■■■■/admin/v2/logs/authentication?limit=100&maxtime=1547666575000&mintime=1531690822000&sort=ts%3Aasc
Jan 16 19:22:55 cyclops-9218 cloudlogshipper[26859]: esduo: sign the request
Jan 16 19:22:55 cyclops-9218 cloudlogshipper[26859]: esduo: canon: Wed, 16 Jan 2019 19:22:55 +0000#012GET#012■■■■#012/admin/v2/logs/authentication#012limit=100&maxtime=1547666575000&mintime=1531690822000&sort=ts%3Aasc
Jan 16 19:22:59 cyclops-9218 cloudlogshipper[26859]: esduo: resp.code is 200, resp.body is {“response”: {“authlogs”: [{“access_device”: {“ip”: “72.219.164.28”, “location”: {“city”: “San Clemente”, “country”: “United States”, “state”: “California”}}, “application”: {“key”: “■■■■”, “name”: “portal”}, “auth_device”: {“ip”: null, “location”: {“city”: null, “country”: null, “state”: null}, “name”: “226-338-7323”}, “event_type”: “enrollment”, “factor”: “sms_passcode”, “reason”: null, “result”: “success”, “timestamp”: 1547242195, “txid”: “42c9b072-9016-423d-8759-5fc6abdcf88a”, “user”: {“key”: “■■■■”, “name”: “carl”}}, {“access_device”: {“ip”: “0.0.0.0”, “location”: {“city”: null, “country”: null, “state”: null}}, “application”: {“key”: “■■■■”, “name”: “macOS”}, “auth_device”: {“ip”: “72.219.164.28”, “location”: {“city”: “San Clemente”, “country”: “United States”, “state”: “California”}, “name”: “226-338-7323”}, “event_type”: “authentication”, “factor”: “duo_push”, “reason”: “user_approved”, “result”: “success”, “timestamp”: 1547242569, “txid”: “02dcf416-1f91-4ea7-a34b-8ca019ae5434”, “user”: {“key”: “■■■■”, “name”: “carl”}}], “metadata”: {“next_offset”: [“1547242569000”, “02dcf416-1f91-4ea7-a34b-8ca019ae5434”], “total_objects”: 2}}, “stat”: “OK”}

Second Request - failure

Jan 16 19:22:59 cyclops-9218 cloudlogshipper[26859]: esduo: https://■■■■/admin/v2/logs/authentication?limit=100&maxtime=1547666579000&mintime=1547666579000&next_offset=1547242569000&next_offset=02dcf416-1f91-4ea7-a34b-8ca019ae5434&sort=ts%3Aasc
Jan 16 19:22:59 cyclops-9218 cloudlogshipper[26859]: esduo: canon: Wed, 16 Jan 2019 19:22:59 +0000#012GET#012■■■■#012/admin/v2/logs/authentication#012limit=100&maxtime=1547666579000&mintime=1547666579000&next_offset=1547242569000&next_offset=02dcf416-1f91-4ea7-a34b-8ca019ae5434&sort=ts%3Aasc
Jan 16 19:23:05 cyclops-9218 cloudlogshipper[26859]: esduo: send the request
Jan 16 19:23:05 cyclops-9218 cloudlogshipper[26859]: esduo: resp.code is 401, resp.body is {“code”: 40103, “message”: “Invalid signature in request credentials”, “stat”: “FAIL”}


#8

Here is the code I changes - it is not very elegant but I am not a Ruby guy. Mostly C++ and Python.

private
def encode_params(params)
	return "" if params.nil?
	if params.is_a?(Hash) then
		params.sort.map do |k,v|
			URI::encode(k.to_s, @@encode_regex) + "=" + URI::encode(v.to_s, @@encode_regex)
		end.join("&")
	else
		# In this case params is an array of hashes - this is needed since
		# some of the requests are able to have repeated parameters.
		# NOTE - if sending in an array of hashes they MUST be already sorted.
                    result = ""
		params.each do |param|
			param = Hash(param)
			param.each do |k,v|
				result += URI::encode(k.to_s, @@encode_regex) + "=" + URI::encode(v.to_s, @@encode_regex)
			end
			result += "&"
		end
		return result.chomp("&")
	end
end

#9

Thanks! I’ll get someone who knows Ruby better than me (which is really anyone who knows Ruby at all) to take a look.


#10

FYI the Perl module has the same issue (can’t handle multiple of the same named params in canonicalize_params()).


#11

FYI the Python module… has this problem too if you call it from duo_client.Admin instead of duo_client.client (i.e. if you write a program)

In particular the call https://github.com/duosecurity/duo_client_python/blob/c69c3■■■■80fadb23eb49ec9/duo_client/admin.py#L322 def get_authentication_log(self, api_version=1, **kwargs): takes keyword args and you cannot repeat the same keyword twice (SyntaxError: keyword argument repeated) and not give it a list as this will fail canonicalization here:

  File "/home/test/project/env/site-packages/duo_client/client.py", line 49, in <genexpr>
    for val in sorted(six.moves.urllib.parse.quote(val, "~") for val in vals):
  File "/usr/lib64/python2.7/urllib.py", line 1296, in quote
    if not s.rstrip(safe):
AttributeError: 'int' object has no attribute 'rstrip'

If you set the timestamp as string, then this can work though, by calling it as such:

get_authentication_log(api_version=2, sort="ts:asc", mintime=31337, next_offset=[[str(31337), txid]], ...) (it also works if you grab it from the metadata reply “as string”

NOTE: I don’t get all results if I don’t use sort="ts:asc" for example. I’m guessing Duo sends back results unordered and they might be outside the mintime:maxtime range, but that’s really confusing if that’s the case. In fact, that’s unusable and if that theory is true (I hope it’s not :slight_smile: then sort should be made required, or have a default.


#12

We’re publishing updates to more of our API client demos now (perl, ruby, and c#) to address this. Thanks for the feedback!