For some background, I'm trying to log-on and automated something behind a portal which uses Duo Security using my username and password (which I would normally input on the portal login website). The exact process I was looking for is detailed in this post by @ajschauer, but I can't get past what they've labeled as Request 5
.
My code is as follows:
TARGET_URL = "https://..."
session = Requests.Session()
# Request 1
r1 = s.get(TARGET_URL)
# Request 2
url2 = r1.url
credentials = { "j_username": "username", "j_password": "password", "_eventId_proceed": ""}
r2 = s.post(url2, data=credentials)
# Parse information from Request 2 to use in Request 3
i1 = r2.text.index("data-host")
i2 = r2.text.index("frameborder")
data = r2.text[i1:i2]
data = data.splitlines()
data_host = data[0].split('"')[1]
data_sig_request = data[1].split('"')[1]
data_sig_request = data_sig_request[:data_sig_request.index(":APP")]
data_post_action = data[2].split('"')[1]
parent = "https://IDPZURL..." + data_post_action
parent = parent.replace('/','%2F').replace(':','%3A').replace(';','%3B').replace('=','%3D').replace('?','%3F')
# Request 3
url3 = "https://" + data_host + "/frame/web/v1/auth?tx=" + data_sig_request + "&parent=" + parent + "&v=2.6"
headers3 = {'Referer': 'https://IDPZURL...'}
s.headers.update(headers3)
r3 = s.post(url3)
# Request 4
url4 = url3.replace('=e1s1', '=e1s2')
formdata4 = {
'tx': data_sig_request,
'parent': url2.replace('=e1s1','=e1s2'),
'v': '2.6',
}
r4 = s.post(url4, data=formdata4)
# Parse information from Request 4 for use in Request 5
ss8 = re.search('value=\"',r4.text)
ss9 = re.search('">\n<input type=\"hidden\" id=\"js_parent\" name=\"js_parent\" value=\"', r4.text)
sid = r4.text[ss8.span(0)[1]:ss9.span(0)[0]]
sid = sid.replace('=','=').replace('|','|')
# Request 5
url5 = "https://" + data_host + "/frame/prompt"
# I'm not too sure on the 'factor' to use because when logging on, I'm never asked for any form of 2FA.
formdata5 = {
'sid': sid,
'factor': 'Duo Push',
'device': 'phone1',
}
r5 = s.post(url5, data=formdata5)
print(r5.text)
Which leads to the output below:
{"stat": "FAIL", "message": "Your session has expired. Please try again."}
Prior to Request 5
, my output in Request 4
contains a valid SID
with the prefix "AUTH...", as the value of a hidden input. Something like the following:
<input type="hidden" id="js_cookie" name="js_cookie" value="AUTH...">
However, to proceed with @ajschauer's solution, I would have to parse information from the text response of Request 5
, which for me is always returning with a status of "FAIL". I've tried looking through the request process by logging on via browser (both through Safari and Edge), and I don't see this POST request to "/frame/prompt", but the data from it is used in subsequent requests by the browsers (i.e., something I should find in Request 5
called txid
).
I've found this GitHub Issue when looking up the exact response from Request 5
, but have found no way to adapt their solution to my code. More specifically, I cannot use their idea of sid=response.json()['response']["sid"]
because none of my requests so far contain any JSON element. As a step further, I've even tried to log on to my target IDPZ url through that same GitHub project (both running it from command-line, and stepping through it with a debugger), and it comes up with errors as well.
Any ideas on how I can get past this issue?