基于SAML2协议改造exchange owa登录——pysaml2深入研究

接上篇,我们成功实现了exchange owa登录不需要输入ad账号密码的demo,那为了能实现后面的各种登录方式,要深入研究下pysaml2登录的流程和原理,方便我们进行二次开发

首先看下exchange和adfs跳转的方式

在我访问exchange owa时,因为没有身份,所以被重定向到了AdfsIssuer

Request URL: https://mail.testyunwei.com/owa/
Request Method: GET
Status Code: 302 

然后ADFS将我们重定向到我们新增的声明提供方,目标地址是声明提供方的saml登录终结点

Request URL: https://adfs.testyunwei.com/adfs/ls/?wa=wsignin1.0&wtrealm=https%3a%2f%2fmail.testyunwei.com%2fowa%2f&wctx=rm%3d0%26id%3dpassive%26ru%3d%252fowa%252f&wct=2021-06-11T09%3a19%3a26Z
Request Method: GET
Status Code: 302 Found

最终到了IDP系统上来,等待IDP完成身份验证

Request URL: https://saml.testyunwei.com:8088/sso/redirect?SAMLRequest=fZFBS8QwEIX%2fSsk9TVp3axq6hWUXoaAiKh68xXTKBtqkZlLX%2ffem7UmFPeZl3rz5eBWqoR%2flfgon%2bwyfE2BImuOOmJbeiNtOb%2fOSQptxulECqNDbggrxUZQtlO2m5CR5A4%2fG2R3J0%2fhqECdoLAZlQ5R4nlFe0Cx75aXMSpkXKef8nSTHmGOsCovzFMKIkrH5lDTEn8tkz2BS7QYpuBAM0TEPrfGgA0kOziLM6ydvpVNoUFo1AMqg5cv%2b4V7GS6Reh%2bRkcQRtOgMtSb6H3qJciK%2b7R%2b%2bC064ndbUQ%2bdV63aQQwc9EpJ6JIpBqO%2fwDtGgsDn4ZDciCnzBUbA2pq7WMx7i6OT653uhLcuf8oK7AZmm2KLGwbhn9jbzve3c%2beFABdiSmAUlYXbH%2frdc%2f&RelayState=28163370-9f40-4cbd-a4f8-c23350ed99bc
Request Method: GET
Status Code: 200 OK

我们的IDP可以从url里获取两个参数:SAMLRequest和RelayState。

RelayState看起来像一个标识符,SAMLRequest是一个加密后的值,参考 djangosaml2idp 源码里的解密代码,进行解密,可以得到如下信息

import xml.dom.minidom
compressed = base64.b64decode(SAMLRequest)
inflated = zlib.decompress(compressed, -15)
dom = xml.dom.minidom.parseString(inflated.decode())
print(dom.toprettyxml())
<?xml version="1.0" ?>
<samlp:LogoutRequest Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" Destination="https://saml.testyunwei.com:8088/slo/redirect" ID="_37ce40ee-d98e-4232-abb6-6a8e84098796" IssueInstant="2021-06-15T01:43:13.133Z" Version="2.0" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
    <Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">http://adfs.testyunwei.com/adfs/services/trust</Issuer>
    <NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" NameQualifier="https://adfs.testyunwei.com:8088/idp.xml" SPNameQualifier="http://adfs.testyunwei.com/adfs/services/trust" xmlns="urn:oasis:names:tc:SAML:2.0:assertion">7af1c646f9558815bb1d7c004743a9264dbf53aef0ce2ffaee648094a139f033</NameID>
    <samlp:SessionIndex>id-RKMFicL4MPmzo1XFS</samlp:SessionIndex>
</samlp:LogoutRequest>

这里面包含了很多信息比如 adfs 终结点的url,nameid,sessionindex等等

分析源码,这里有几个有用的参数被保存进了session:

  1. RelayState

  2. samlp:LogoutRequest[‘ID’] 保存在 destination 变量内

获取了必须的数据后,返回了我们登录验证的页面,然后是处理账号密码验证的逻辑

Request URL: https://saml.testyunwei.com:8088/verify
Request Method: POST
Status Code: 302 Found

登录成功后,会被重定向到soo/redirect登录url

Request URL: https://saml.testyunwei.com:8088/sso/redirect?id=HDca0LudApPvE6FUBHkYTI5u&key=8ba47480e949b298a866fed9d2ebd5ee0759dec7
Request Method: GET
Status Code: 200 OK

经过一系列的判断和执行后,最后到了下面两段代码

_resp = IDP.create_authn_response(
    identity,
    userid=self.user,
    encrypt_cert_assertion=encrypt_cert,
    **resp_args
)
http_args = IDP.apply_binding(
    self.binding_out,
    "%s" % _resp,
    self.destination,
    relay_state,
    response=True,
    **kwargs
)

identity 变量是我们最终获取的用户信息,这里他的值是,也就是我们最初设置的用户的upn

 {"upn": "test01@testyunwei.com"}

self.destination 是 SAMLRequest 解密出来的 samlp:LogoutRequest[‘ID’] 的值

relay_state 是 RelayState

最终,http_args 会得到一个html的格式的内容,把http_args直接通过httpresponse到前端后,就会自动跳转adfs完成登录了。

我们模拟登陆一次后,抓取到http_args 内容如下

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
  </head>
  <body onload="document.forms[0].submit()">
    <noscript>
      <p>
          <strong>Note:</strong>
          Since your browser does not support JavaScript,
          you must press the Continue button once to proceed.
      </p>
      </noscript>
      <form action="https://adfs.testyunwei.com/adfs/ls/" method="post">
          <input type="hidden" name="SAMLResponse" value="25zMDpSZXNwb25zZT4K"/>
          <input type="hidden" name="RelayState" value="add3499b-3fd2-4726-b0f6-4dd8201e014e"/>
          <noscript>
                <input type="submit" value="Continue"/>
          </noscript>
      </form>
  </body>
 </html>

这里 SAMLResponse input字段的value很长很长,我进行了截取,只留了一小部分。很明显可以看出来,最终会将 SAMLResponse 和 RelayState 的值由form表单提交至 https://adfs.testyunwei.com/adfs/ls/ 。那么我们后面自己实现的时候,最好将 http_args 自动跳转逻辑改写,由后端http返回json,前端统一跳转

我这里抓取了一下 saml response 后的 xml,红框内可以看到,里面包含了 用户的upn

到这里对 pysaml2 基本流程和逻辑也都大致清楚了,我们系统的架构图大致如下