谁从饼干罐中拿走了 python 请求 shib_idp_session 饼干?

Who took the python requests shib_idp_session cookies from the cookie jar?

我正在尝试使用 python (3.9.1) 和请求 (2.25.1) 通过 Shibboleth 和 Duo 双因素身份验证 (2FA) 登录。我拥有这样做的所有适当的凭据,而且,我经常使用浏览器登录。最终,我试图将成功登录后发生的一些任务自动化。事实上,我仅通过 Shibboleth 和 成功登录。但是现在,我的机构需要 Duo 2FA,我必须重新编写代码的这个身份验证部分。我几乎可以正常工作,但几乎在最后一步就失败了。我一直在使用 Chrome 开发人员工具来跟踪流程,并将其用作发出哪些请求以及在每个请求中传递哪些信息的指南。在此 post 中,我将不想分享的好东西替换为“XXXXXXXXXXX”。

使用 session object 的请求流程如下。请注意,我在每个请求描述的末尾以斜体显示请求响应 重定向历史记录,并以粗体显示 状态代码。没有斜体代码意味着没有重定向。我所有的代码都在这个文字描述下面。

  1. 请求 1 - 请求最终的 URL 我希望在其中获取数据并开始我的自动化。 302, 302, 200
  2. 请求 2 - session 处理重定向到基于 url 的 IDP SAML,并且需要我的用户名和密码。 302, 200
  3. 请求 3 - 成功传递凭据后,session 被传递给 duo。 Duo 检查我的 IDP 身份验证。 302, 200
  4. 请求 4 - IDP 身份验证成功后,duo 提示我的设备进行 2FA,我确实收到了此提示。 200
  5. 请求 5 - 询问二人组 api 状态。 Duo 回答 'OK' 并且登录请求是 'pushed'。 200
  6. 请求 6 - 再次询问二人组 api 状态。 Duo 回答 'Success' 并提供 duo 2FA 最后一步所需的 txid。 200
  7. 请求 7 - Post txid 到 duo api。 200
  8. 请求 8 - 此 url 作为对请求 2 的响应给出,并作为 parent 通过 duo。这里的成功请求将给我 RelayState 和 SAMLResponse。我成功获得了这两个变量,是的,SAMLResponse 有时接近 50,000 个字符。 200
  9. 请求 9 - Post 对 Shibboleth 的 RelayState 和 SAMLResponse。 500
  10. 请求 10 - 最后,我希望到达我的最终目的地 URL 但到目前为止,我无法通过请求 9。

请求 9 的响应文本的重要部分是:

请求 session cookie jar 似乎成功地获取了我在挖掘 Chrome DevTools 时看到的所有 cookies,除了请求 8 之外的所有上述请求。在 Chrome DevTools 中,这个响应 Header 包含五个“Set-Cookie”实例,我在 cookie 罐中只看到其中一个。我确实观察到的一个 cookie 是“BIGipServer~prt_shib~pl_idpXXXXXXX”,它在请求 #1 后被放置在 cookie 罐中。我在请求 8 的响应中没有观察到但在 Chrome DevTools 中观察到的缺失 cookie 是“shib_idp_session_ss”、“XXXXX-rememberme-XXXXXXXXX”、“XXXX-optin-XXXXXXXXXX”、和“shib_idp_session”。我关注这些 cookie 是因为:(1) 它们是 Chrome DevTools 和我的请求 session 之间的区别,以及 (2) 在搜索失败请求 9 的响应文本时,我看到各种 post 关于 cookie 问题的网络。因为我每次都从头开始重新启动 session,所以我不应该有任何过时的 cookie 或其他活动登录。关于活动登录,我可以使用相同的凭据同时使用 firefox、chrome 和 safari 登录。

为了解决我的问题,我专注于这些 shib_session cookie 是否正确?由于我没有通过这些 cookie,我是否应该在请求流中寻找更高的位置(例如在 duo 之前)?如果我成功收到 RelayState 和 SAMLResponse,为什么我也没有收到 shib_session cookie?

最后,我知道 http 请求可能对 headers 敏感。我天真地使用 status-code 作为让 header 正确的指标。也许我在早期缺少 header 规范,然后会提示 IDP 给我 shib cookies?

这是我的代码:

import re
import requests

## start requests session
s = requests.Session()

## Request 1
url1 = '[  ultimate target URL  ]' # This is the ultimate target URL
r1 = s.get(url1)

## Request 2
url2 = r1.url # redirection url from r1 is next up and used as url2
cred = {'j_username': 'XXXXXXXXXX', 'j_password': 'XXXXXXXXXX', '_eventId_proceed' : 'Sign in'}
r2 = s.post(url2, data = cred)

## Request 3
ss3 = re.search('data-host="',r2.text)
ss4 = re.search('"\n                data-sig-request="',r2.text)
data_sig_request = r2.text[ss4.span(0)[1]:ss5.span(0)[0]]
ss6 = re.search('data-post-action="',r2.text)
ss7 = re.search('"\n                frameborder=',r2.text)
data_post_action = r2.text[ss6.span(0)[1]:ss7.span(0)[0]]
prnt = '[  idp url  ]' + data_post_action
prnt = prnt.replace('/','%2F').replace(':','%3A').replace(';','%3B').replace('=','%3D').replace('?','%3F')
url3 = 'https://api-XXXXXXXXXX.duosecurity.com/frame/web/v1/auth?tx=' + data_sig_request + '&parent=' + prnt + '&v=2.6'
headers3 = {'Referer': '[  idp url  ]'}
s.headers.update(headers3)
r3 = s.post(url3)

## Request 4 - duo prompt
url4 = 'https://api-XXXXXXXXXX.duosecurity.com/frame/prompt'
ss8 = re.search('value="',r3.text)
ss9 = re.search('">\n<input type="hidden" name="url" value="',r3.text)
sid = r3.text[ss8.span(0)[1]:ss9.span(0)[0]]
sid = sid.replace('&#x3d;','=').replace('&#x7c;','|')
ss10 = re.search('"ukey" value="',r3.text) 
ss11 = re.search('">\n\n<input type="hidden" name="out_of_date"',r3.text)
formdata4 = {'sid':sid, 'device':'phone1', 'factor':'Duo Push'}
r4 = s.post(url4,formdata4)

## Request 5 - Duo Status
url5 = 'https://api-XXXXXXXXXX.duosecurity.com/frame/status'
sid5 = sid.replace('=','%3D').replace('|','%7C')
ss12 = re.search('"txid": "',r4.text)
ss13 = re.search('"}}',r4.text)
txid = r4.text[ss12.span(0)[1]:ss13.span(0)[0]]
formdata5 = {'sid':sid, 'txid':txid}
r5 = s.post(url5,formdata5)

## Request 6 - second status call to duo
url6 = url5
formdata6 = formdata5
r6 = s.post(url6,formdata6)

## Request 7 - Duo - evaluate push
url7 = 'https://api-XXXXXXXXXX.duosecurity.com/frame/status/' + txid  
formdata7 = {'sid':sid}
r7 = s.post(url7,formdata7)

## Request 8
url8 = r2.url # r2.url is the redirection url from url2
ss18 = re.search('"\n                data-post-action',r2.text)
sig_response = r2.text[ss4.span(0)[1]:ss18.span(0)[0]]
sig_response = sig_response.replace('TX|','AUTH|')
formdata8 = {'_eventId':'proceed', 'sig_response':sig_response}
r8 = s.post(url8,formdata8)

## Request 9 - should redirect to ultimate url (url1)
url9 = 'https://XXXXXXXXXXXX/Shibboleth.sso/SAML2/POST'
ss19 = re.search('name="RelayState" value="',r8.text)
ss20 = re.search('"/>                \n                                \n                <input type="hidden" name="SAMLResponse" value="',r8.text)
RelayState = r8.text[ss19.span(0)[1]:ss20.span(0)[0]]
RelayState = RelayState.replace('&#x3a;',':')
ss21 = re.search('"/>                \n            </div>\n            <noscript>\n                <div>\n                    <input type="submit" value="Continue"/>',r8.text)
SAMLResponse = r8.text[ss20.span(0)[1]:ss21.span(0)[0]]
formdata9 = {'RelayState':RelayState, 'SAMLResponse':SAMLResponse}
r9 = s.post(url9,formdata9,allow_redirects=True)

## Request 10 - this would be the ultimate final destination URL and is url1 from above.

我终于解决了这个问题。问题出在请求 8 中。在我知道问题出在请求 8 中之前,我使用 https://www.samltool.com/decode.php 解码了我的 SAMLResponse,结果发现我的身份验证失败了。请求 8 的问题是 sig_response。它只包含了一半的必需信息。一个正确的 sig_response 应该首先有 DUO 响应 cookie,其次是来自请求 2 的 data-sig-request 的后半部分。

通过正确组装 sig_response,idp returns 缺少 shib_idp_session cookie,还有一个可接受的 SAMLResponse 密钥。另请注意,此成功脚本中使用了 python 请求默认值 headers。

这是我的代码,根据问题稍微重构并修复了错误:

import re
import requests
import time

## start HTTP request session
s = requests.Session()

## Request 1
url1 = '[  ultimate target URL  ]'
r1 = s.get(url1)

## Request 2
url2 = r1.url
cred = {'j_username': 'XXXXXXXXXX', 'j_password': 'XXXXXXXXXX', '_eventId_proceed' : 'Sign in'}
r2 = s.post(url2, data = cred)

## Request 3
ss4 = re.search('"\n                data-sig-request="',r2.text)
ss5 = re.search(':APP',r2.text)
data_sig_request = r2.text[ss4.span(0)[1]:ss5.span(0)[0]]
ss6 = re.search('data-post-action="',r2.text)
ss7 = re.search('"\n                frameborder=',r2.text)
data_post_action = r2.text[ss6.span(0)[1]:ss7.span(0)[0]]
prnt = '[  idp url  ]' + data_post_action
prnt = prnt.replace('/','%2F').replace(':','%3A').replace(';','%3B').replace('=','%3D').replace('?','%3F')
url3 = 'https://api-XXXXXXXXXX.duosecurity.com/frame/web/v1/auth?tx=' + data_sig_request + '&parent=' + prnt + '&v=2.6'
r3 = s.get(url3)

## Request 4
url4 = url3
formdata4 = {'tx': data_sig_request, 'parent': url2.replace('=e1s1','=e1s2')}
r4 = s.post(url4, data=formdata4)

## Request 5
url5 = 'https://api-XXXXXXXXXX.duosecurity.com/frame/prompt'
ss8 = re.search('value="',r4.text)
ss9 = re.search('">\n<input type="hidden" name="url" value="',r4.text)
sid = r4.text[ss8.span(0)[1]:ss9.span(0)[0]]
sid = sid.replace('&#x3d;','=').replace('&#x7c;','|')
ss10 = re.search('"ukey" value="',r4.text) 
ss11 = re.search('">\n\n<input type="hidden" name="out_of_date"',r4.text)
formdata5 = {'sid':sid, 'device':'phone1', 'factor':'Duo Push', 'days_to_block': 'None'}
r5 = s.post(url5, data = formdata5)

## Request 6
url6 = 'https://api-XXXXXXXXXX.duosecurity.com/frame/status'
ss12 = re.search('"txid": "',r5.text)
ss13 = re.search('"}}',r5.text)
txid = r5.text[ss12.span(0)[1]:ss13.span(0)[0]]
formdata6 = {'sid':sid, 'txid':txid}
r6 = s.post(url6, data = formdata6)

## Request 7
url7 = url6
formdata7 = formdata6
r7 = s.post(url7, data = formdata7)

## Request 8
url8 = 'https://api-XXXXXXXXXX.duosecurity.com/frame/status' + txid
formdata8 = {'sid':sid}
r8 = s.post(url8, data = formdata8)

## Request 9
url9 = r2.url
ss14 = re.search('APP',r2.text)
ss15 = re.search('"\n                data-post-action="',r2.text)
r8td = ast.literal_eval(r8.text)
data_sig_request_APP = r2.text[ss14.span(0)[1]+1:ss15.span(0)[0]]
sig_response = r8td['response']['cookie'] + ':APP|' + data_sig_request_APP
formdata9 = {'_eventId':'proceed',
    'sig_response':sig_response}
r9 = s.post(url9, data = formdata9)

## Request 10
url10 = '[  ultimate target URL  ]/Shibboleth.sso/SAML2/POST'
ss19 = re.search('name="RelayState" value="',r9.text)
ss20 = re.search('"/>                \n                                \n                <input type="hidden" name="SAMLResponse" value="',r9.text)
RelayState = r9.text[ss19.span(0)[1]:ss20.span(0)[0]]
RelayState = RelayState.replace('&#x3a;',':')
ss21 = re.search('"/>                \n            </div>\n            <noscript>\n                <div>\n                    <input type="submit" value="Continue"/>',r9.text)
SAMLResponse = r9.text[ss20.span(0)[1]:ss21.span(0)[0]]
formdata10 = {'RelayState':RelayState, 'SAMLResponse':SAMLResponse}
r10 = s.post(url10, data = formdata10)```