文章

SSO 单点登录的明星协议 SAML2.0

简介

SAML 是 2001 年问世的古董的协议。相比于 2010 年成为 RFC 标准的 OAuth2.0,即使是 SAML2 也早在 2005 年就发布了,廉颇老矣,尚能饭否?答曰:能。事实上直到今天(2024年)SAML2.0 依然是不少企业实现 SSO 的首选协议,这其中既少不了 Cisco 等不思进取的传统企业的推波助澜,也是许多大型公司屎山过重基于兼容性考虑的不得已而为之,当然,SMAL2.0 也有它自己的优点。

如无特别说明,本文 SAML 均指 SAML 2.0。

SAML 同时适用于认证 (Authentication / AuthN) 与授权 (Authroization / AuthZ)。考虑到部分同学可能傻傻分不清,这里就浅显地再解释下:

  • 认证 (AuthN) 指的是识别用户身份,与「登录」属于同一个领域,认证失败常用 HTTP 状态码 401 表示。
  • 授权 (AuthZ) 指的是已经识别用户身份的情况下,判断此人具有哪些权限,授权失败常用 HTTP 状态码 403 表示。

授权通常基于认证,但是广义来说未登录(认证)的用户也可以有权限,例如「游客」角色。

作为古董协议,SAML 使用 XML 格式来传输数据。

术语

作为一个古老的协议,它使用的许多术语与当今技术生态格格不入,但实际上都有类似的平替概念,这里快速过一下以免下文看的一脸懵逼。

为了便于理解,我们假设一个场景:「用户使用公司邮箱登录 Zoom,而他们公司使用 Auth0 管理账号」

SAML 牵扯 3 个实体(参与方),它们分别是:

  • User Agent: 也许你对这个单词很熟悉,的确,大部分情况下指的就是浏览器。确切的说是用户使用的客户端程序。
  • SP: 全称 Service Provider,即用户想访问(登录) 的系统。
  • IDP: 全称 Identify Provider,即提供认证的服务,通常也是账号储存的地方。

在上面假设的场景中,Zoom 客户端(或网页)是 User Agent,Zoom 是 SP,Auth0 是 IDP。需要注意的是 SP 一般特指后端程序,而 UA 指的是用户使用的前端。

这三个实体之间的通信涉及以下名词:

  • SAML Assertion: 平替为 JWT Token,即按照固定格式储存了用户身份数据的 XML 字符串,配有相应的签名防止篡改或伪造。
  • SAML Metadata: SP 与 IDP 配置具体通信协议/数据格式的配置文件,包含了一方所需的全部配置项。通常以 XML 文件,或一个返回文件内容的 URL 的形式提供。也可以手动配置无需 Metadata 文件。
  • Bindings: 指明 IDP 与 SP 之间如何传输数据(发送请求)。
  • ACS: 全称 Assertion Consumer Service,指 SP 接受 SAML Accertion 的 endpoint(一个 URL)。用户登录后 IDP 得把结果告诉 SP 吧,就通过这个。

流程

启动 SAML 认证(登录)流程有两种方式:

  • SP-INIT
  • IDP-INIT

SP-INIT

顾名思义,此方式中由 SP 发起登录请求。在上文假设的场景中,就是用户直接访问 Zoom,然后从 Zoom 跳转到公司网站(很可能是 Auth0 托管的)来登录。

具体流程如下:

  1. 用户访问 SP。(用户访问 Zoom)
  2. SP 发现用户未登录,将其重定向到 IDP 。通常 SP 会提供多种登录方式,只有用户选择 SSO 时 SP 才会重定向到对应的 IDP。(用户在 Zoom 点击 SSO 跳转到公司登录页)
  3. 用户在 IDP 提供的网页中完成登录,IDP 生成已签名的断言 (Assertion),将用户重定向回 SP。
  4. SP 读取断言,校验签名,取得用户数据,视为已登录。

IDP-INIT

与 SP-INIT 相反,IDP-INIT 中由 IDP 主动发起登录,然后跳到 SP。这种情况不多见,仅有的用例是企业可能会提供一个面板(书签),里面有他们所购买的所有外部服务的入口。员工从这里点进去直接就是已登录的状态。

流程如下:

  1. 用户访问 IDP。(用户访问公司内网)
  2. 完成登录后 IDP 生成已签名的断言 (Assertion)。
  3. 用户被重定向到 SP,SP 读取并验证断言,视为用户已登录。

注意不是所有的 SP 都接受 IDP-INIT。

Metadata 配置文件

一个典型的 IDP 提供给 SP 的配置文件如下(省略了命名空间等):

<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="https://sso.example.com/saml2/sp/xxxxx">
  <md:IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
    <md:KeyDescriptor use="signing">
      <ds:KeyInfo>
        <ds:X509Data>
          <ds:X509Certificate>xxxxx</ds:X509Certificate>
        </ds:X509Data>
      </ds:KeyInfo>
    </md:KeyDescriptor>
    <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
    <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
    <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat>
    <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
    <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sso.example.com/saml2/sp/xxxxx/sso"/>
    <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.example.com/saml2/sp/xxxxx/sso"/>
  </md:IDPSSODescriptor>
</md:EntityDescriptor>

顶级元素 md:EntityDescriptor 定义了一个 SAML 实体,在这个具体的例子中是一个 IDP。其属性 entityID 是此实体的唯一标识符,可以为任意字符串,但为了避免冲突通常都是一个 URL。

次级元素 IDPSSODescriptor 定义了 SSO 相关的信息。虽然理论上 SAML 不仅仅用于 SSO,但事实上这几乎是 SAML 唯一的用例(要不是被逼的,谁会用如此古老反人类的协议呢)。

元素 KeyDescriptor 包含了此实体的公钥(一般是 X.509 格式的,就是最常见的那个,HTTPS 用的那种格式啦)。可能有多个 KeyDescriptor 元素,通过属性 use 指明用途,例如 signingencryption

元素 NameIDFormat 指明了IDP 能提供的 NameID 格式(若是 SP 生成给 IDP 的 metadata 则表示 SP 希望得到的 NameID 格式)。常见的有(省略前缀):

  • unspecified: 由 IDP 自行决定(可能是任意字符串)。

  • emailAddress: 电子邮箱。

  • persistent: 永久 ID,即每次登录都不变,可用于映射 SP 本地用户。

  • transient: 临时 ID,同一个用户下次登录可能就变了。

元素 SingleSignOnService 指明了实现单点登录的信息,通过 BindingLocation 属性指示 SP 应以何种方式请求哪个地址。常见 Binding 类型如下(省略前缀):

  • HTTP-Redirect: 多用于 SP 主动请求认证。说人话就是把用户重定向到这个地址(附带一些参数,比如编码后的 SAMLRequest),他们就可以登录了。
  • HTTP-POST: 用于发送一些较大的数据。(HTTP GET URL 长度有限,用 POST 可解决)

实例

这里演示一个 SP-INIT 实现登录的完整请求流程。简单起见,具体的 SSO 实现由 aws cognito 代劳。

重定向到 IDP

首先我们的业务系统(也就是 SP)发现用户未登录,给出几个登录选项,当用户选择某家 SSO 登录时,跳转到对应的 IDP。这里的目标 URL 形如 https://sso.example.com/saml2/sp/xxxxx/sso?SAMLRequest=xxxx&RelayState=yyyy&SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256&Signature=ssss

稍微有点乱,我们拆解一下(注意查询参数部分都经过了 URL 编码)。

Host 与 Path

https://sso.example.com/saml2/sp/xxxxx/sso 与 IDP 提供的 metadata 中 Binding 相关配置一致。此处也进一步说明了 Binding 的意义与作用:即指明如何传输数据。

SAMLRequest

这个查询参数的值是 XML 格式的 SAML 请求数据,但经过了 zip 压缩 + base64 编码。还原后如下:

<?xml version="1.0" encoding="UTF-8"?>
<saml2p:AuthnRequest 
    AssertionConsumerServiceURL="https://myserver.com/saml2/idpresponse" 
    Destination="https://sso.example.com/saml2/sp/xxxxx/sso" 
    ID="_6bd701a4-f3dc-46fc-899a-003a2782cbea"
    IssueInstant="2024-05-18T18:05:39.843Z"
    Version="2.0"
    xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol"
>
    <saml2:Issuer
        Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity"
        xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"
    >
        urn:amazon:cognito:sp:xxxx
    </saml2:Issuer>
    <saml2p:NameIDPolicy
        AllowCreate="false"
        Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
    />
</saml2p:AuthnRequest>

直白解释下,这个请求包含了这些信息:

  • 请求谁 (Destination):一般就是 IDP 提供的 URL。
  • 响应给谁 (AssertionConsumerServiceURL):一般是 SP 专门接收 SAML 响应的 URL。
  • 谁发出的请求 (saml2:Issuer):这里是 aws cognito,如果是自己实现的 SAML 客户端可以自定义此字段。
  • 希望得到的用户名类型 (saml2p:NameIDPolicy):注意这里只是建议,IDP 有最终决定权。

RelayState

这个参数在 SAML 层面没有意义,IDP 不会处理这个参数,但会在给 SP 的响应(回调)中原样带回。设计目的与它名字一样,是方便 SP 保留状态。包括 OAuth 在内的其他协议也有类似的设计。

签名相关

SigAlg 指明签名使用的算法。Signature 是真正的签名,由请求发送者(此处是 SP)使用自己的私钥对 SAMLRequest 签名。如果接收方(此处是 IDP)想验证发送者的身份,需要预先配置对方的公钥。

返回 SP

用户在 IDP 处完成登录后 IDP 签署 SAML 响应并回调 SP,也就是 ACS,通常以 HTTP POST 形式完成。

在这个实例中,从浏览器的开发者工具可以看到 https://myserver.com/saml2/idpresponse 的 POST 请求,请求体是 application/x-www-form-urlencoded 格式的数据,包括两个字段。其中一个是 RelayState,前面已经说过了,就是原样回带 SP 请求时的值。

重点看 SAMLResponse,因为这次是放在 POST 请求体中的,没有长度与字符限制,所以一般就不压缩了,直接 base64 解码可得原文:

<samlp:Response>
    <saml:Issuer>...</saml:Issuer>
    <ds:Signature>...</ds:Signature>
    <samlp:Status>
        <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
    </samlp:Status>
    <saml:Assertion IssueInstant="2024-05-18T18:05:41Z" Version="2.0">
        <saml:Issuer>...</saml:Issuer>
        <ds:Signature>...</ds:Signature>
        <saml:Subject>
            <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">example@example.org</saml:NameID>
            ...
        </saml:Subject>
        <saml:Conditions NotBefore="2024-05-18T18:05:11Z" NotOnOrAfter="2024-05-18T18:10:41Z">
            <saml:AudienceRestriction>
                <saml:Audience>urn:amazon:cognito:sp:xxx</saml:Audience>
            </saml:AudienceRestriction>
        </saml:Conditions>
        <saml:AuthnStatement>...</saml:AuthnStatement>
        <saml:AttributeStatement>
            <saml:Attribute
                Name="app.email"
                NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
                >
                <saml:AttributeValue xsi:type="xs:string">example@example.org</saml:AttributeValue>
            </saml:Attribute>
        </saml:AttributeStatement>
    </saml:Assertion>
</samlp:Response>

作为古老的企业协议,响应有点复杂和冗余也可以理解对吧。不过需要重点关注的也就那么几部分:

  • 签名ds:Signature 保存了 IDP 的签名信息,SP 应该使用预先配置的 IDP 公钥校验签名的正确性,防止有人伪造 SAML Response。

  • Subjectsaml:NameID 标签包含了用户的唯一 ID 以及其格式(类型)。SP 通常应该通过这个字段将外部用户与自己的用户数据库建立关联。

  • Conditions:这里列出了当前 SAML 断言的生效前提,一般是限制有效期,也有可能限制接收者。不满足条件的断言应当被视为无效,防止可能的安全问题。

  • AttributeStatement:IDP 额外提供的用户信息字段(通常需要告知 IDP 维护人员手动配置)。

SP 收到这个请求后一般应该返回一个 HTTP 302,将用户重定回到自己的页面。至此,从用户视角看,完成了整个登录流程:

  1. 从 SP 跳转到 IDP
  2. 完成登录
  3. 跳转回 SP

从服务器视角,SP 利用 HTTP GET 向 IDP 发送了登录请求,IDP 通过 HTTP POST 将 SAML 断言传递给 SP。

SAML 与 OAuth 的异同

显然 SAML 与 OAuth 都可以实现第三方登录,但细节上有许多区别,也正是这些区别导致了 SAML 成为企业级 SSO 的首选协议,而 OAuth 则霸占了个人账户市场。

为了方便表述,此处统一使用 SAML 中术语。

SAMLOAuth
信任关系服务商与组织服务商与最终用户
交互性弱,仅用于登录强,双向交互
单点注销支持不支持

信任关系

SAML 中信任关系在 SP 与 IDP 之间直接建立,最终用户不需要关心 SP 是否值得信任。说人话就是,如果一个服务商 A 需要通过 SAML 接入其客户 B 的系统,那么就需要这个 A 的负责人与 B 的负责人线下交流,两家公司(组织)级别信任后交换配置,完成 SSO 接入。B 的员工通过工作账号登录 A 时无需关心 A 是否可信,B 的系统通常也不会询问 B 是否允许将其账户信息共享给 A。

OAuth 则相反,通常 IDP 不会严格审查 SP 的资质。用户通过 OAuth 登录服务商 A 时,ISP 会询问用户是否允许 A 获取其账号信息。一个典型例子是任何人都可以轻松在 GitHub 注册 OAuth 客户端,从而让自己的系统可以支持 GitHub 登录。显然 GitHub 并不为这个系统做任何担保,一切操作都需要用户授权,同时 GitHub 也会提醒用户登录时确认 A 是否可信。

说白了 SAML 中账号被视为公司资产,决定权属于 IDP。OAuth 更强调账号的私密性,决定权在用户。

交互性

在实践上,SAML 更多的是单一的认证。即 IDP 把用户信息发送给 SP 就完事了,SP 后续基本不会再与 IDP 沟通。

OAuth 则更注重交互性,登录只是第一步,此时 SP 可获得 IDP 颁发的令牌。通过这个令牌 SP 可以随时请求 IDP,例如获取更多数据,以用户的名义执行一些操作(比如发帖等)。

也正是因为 OAuth 的交互性更丰富,它的认证流程还包括了鉴权,SP 请求时需要明确申请权限(例如是只获取基本信息,还是需要以用户名义发帖),用户登录时可对不同的 SP 授予不同的权限。

单点注销

经过配置 SAML 可实现单点注销。例如公司 B 的用户通过 SAML 登录了 A 服务。若此用户在 A 处注销登录,那么他在公司网站上也退出登录了。尽管实践上大部分系统不会配置单点注销,但 SAML 提供了这项能力。

OAuth 原生不支持注销功能。这一点我们应该深有体会,例如通过微信登录美团后,退出美团不会影响微信的登录状态。

常见问题

IDP 一定是储存账号数据的地方吗?

不一定。IDP 其实只是提供 SAML 服务而已,IDP 程序可能从其他位置,通过其他协议读取账号数据。互联网协议不就是不断套娃嘛。但这些对 SP 来说是透明的,SP 只管和 IDP 交互就行了。

SP 通过 SAML 接入 SSO 后,自己的用户数据库扮演什么角色?

具体要看 SP 的系统设计。通常来讲大部分 SP 会把从 IDP 得到的用户数据映射到一个本地用户(即 SP 自己的用户),毕竟还是和自己的数据库交互舒服呀。至于映射的逻辑,以及本地用户不存在时的行为,就看业务需求了。但是注意,大部分情况下通过 IDP 映射的本地用户应该与真正的本地账号隔离——不应该允许从 SAML SSO 以外的渠道登录。

附录

IDP Metadata

<md:EntityDescriptor xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" xmlns:xenc11="http://www.w3.org/2009/xmlenc11#" entityID="https://sso-2781261d.sso.duosecurity.com/saml2/sp/DIY0T3WG6QRVD2U1SI1F/metadata">
  <md:IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
    <md:KeyDescriptor use="signing">
      <ds:KeyInfo>
        <ds:X509Data>
          <ds:X509Certificate>MIIDDTCCAfWgAwIBAgIUP1HDN7PUbmwWMl+nPxKrvWaYhHkwDQYJKoZIhvcNAQEL
BQAwNjEVMBMGA1UECgwMRHVvIFNlY3VyaXR5MR0wGwYDVQQDDBRESVkwVDNXRzZR
UlZEMlUxU0kxRjAeFw0yNDA0MjMxMDUzMjdaFw0zODAxMTkwMzE0MDdaMDYxFTAT
BgNVBAoMDER1byBTZWN1cml0eTEdMBsGA1UEAwwURElZMFQzV0c2UVJWRDJVMVNJ
MUYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzVye7WgL2ntwoDt3a
Pp+0VNGLWDve1KQAPc6u+qAsMqszTULudNHeEUDcBru8EQjahN4K7ID5CstOKmuy
70Ob5grzC+bldQcYpcYhNqEfy/5Ycv91U6jeCqWEiNuBxxWlA63rVdfWJ3TF6Rh5
2Phd76f008afN9GZAsgajik/7jxakofHHDSNGZTnvv65kJz+YglE8byEswKuzPv6
oioaVGgF5oI9yRoFi6vSA1o/t74nBfd4BzV8AWUD3065D6mHYOSABFimlnNqRno2
TT43PHsgszpJUE/RxxstDlp13FfXdQMSxJj5E5Zm2tFZa13h0xIKehuXGbAqHpC0
Q7uZAgMBAAGjEzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB
AExG2xlV9iubyV0fl0SbsEBGO56Jel+QabvX3TOy5joKBp0kw8MN8YjWxAV6Er5Q
Ti/qXJf9ckwu9nGnmX1bSmkwZ9Ep0VgslTVVsZ9h1HQt/VcT9bxSTFeh0KMvktQ+
RlewOjCJSTfnOOTrGYgzdfVVZXvL+xUkey/DEAmLkg+ilBSoxy8hOHIq/OG1l15P
uNOf5UmVOpH4HJXvexGMrpRDeXNpIb/Dfv4RlCIqStPdty1CFpoZBaaMasoOk3Rr
p5g5zzysWwrHMYr0ZWNQAzQZ6OSWfsKo5qssY8Rpxbl/WNk/zpAO95ClgccpSGAd
kWgMs7M565vs6vUmOiQWvwA=
</ds:X509Certificate>
        </ds:X509Data>
      </ds:KeyInfo>
    </md:KeyDescriptor>
    <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
    <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
    <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat>
    <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
    <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sso-2781261d.sso.duosecurity.com/saml2/sp/DIY0T3WG6QRVD2U1SI1F/sso"/>
    <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso-2781261d.sso.duosecurity.com/saml2/sp/DIY0T3WG6QRVD2U1SI1F/sso"/>
  </md:IDPSSODescriptor>
</md:EntityDescriptor>

SAML Request

<?xml version="1.0" encoding="UTF-8"?>
<saml2p:AuthnRequest AssertionConsumerServiceURL="https://cognito.policlear.org/saml2/idpresponse" Destination="https://sso-2781261d.sso.duosecurity.com/saml2/sp/DIY0T3WG6QRVD2U1SI1F/sso" ID="_6bd701a4-f3dc-46fc-899a-003a2782cbea" IssueInstant="2024-05-18T18:05:39.843Z" Version="2.0"
    xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol">
    <saml2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity"
        xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">urn:amazon:cognito:sp:eu-west-1_kqYKuz6Aj
    </saml2:Issuer>
    <saml2p:NameIDPolicy AllowCreate="false" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"/>
</saml2p:AuthnRequest>

SAML Response

<samlp:Response
    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
    xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
    xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
    xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"
    xmlns:xenc11="http://www.w3.org/2009/xmlenc11#" Version="2.0" ID="DUO_a4fdf2812dee053117dd6d9d71a7bb94bed80394" IssueInstant="2024-05-18T18:05:41Z" Destination="https://cognito.policlear.org/saml2/idpresponse" InResponseTo="_6bd701a4-f3dc-46fc-899a-003a2782cbea">
    <saml:Issuer>https://sso-2781261d.sso.duosecurity.com/saml2/sp/DIY0T3WG6QRVD2U1SI1F/metadata</saml:Issuer>
    <ds:Signature>
        <ds:SignedInfo>
            <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
            <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
            <ds:Reference URI="#DUO_a4fdf2812dee053117dd6d9d71a7bb94bed80394">
                <ds:Transforms>
                    <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
                    <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                </ds:Transforms>
                <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
                <ds:DigestValue>+cg2uZOb0Y4qaif83LOdCpcBSAXGOjGgqJL8Gr5qZNQ=</ds:DigestValue>
            </ds:Reference>
        </ds:SignedInfo>
        <ds:SignatureValue>xxxxx==</ds:SignatureValue>
        <ds:KeyInfo>
            <ds:X509Data>
                <ds:X509Certificate>MIIDDTCCAfWgAwIBAgIUP1HDN7PUbmwWMl+nPxKrvWaYhHkwDQYJKoZIhvcNAQELBQAwNjEVMBMGA1UECgwMRHVvIFNlY3VyaXR5MR0wGwYDVQQDDBRESVkwVDNXRzZRUlZEMlUxU0kxRjAeFw0yNDA0MjMxMDUzMjdaFw0zODAxMTkwMzE0MDdaMDYxFTATBgNVBAoMDER1byBTZWN1cml0eTEdMBsGA1UEAwwURElZMFQzV0c2UVJWRDJVMVNJMUYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzVye7WgL2ntwoDt3aPp+0VNGLWDve1KQAPc6u+qAsMqszTULudNHeEUDcBru8EQjahN4K7ID5CstOKmuy70Ob5grzC+bldQcYpcYhNqEfy/5Ycv91U6jeCqWEiNuBxxWlA63rVdfWJ3TF6Rh52Phd76f008afN9GZAsgajik/7jxakofHHDSNGZTnvv65kJz+YglE8byEswKuzPv6oioaVGgF5oI9yRoFi6vSA1o/t74nBfd4BzV8AWUD3065D6mHYOSABFimlnNqRno2TT43PHsgszpJUE/RxxstDlp13FfXdQMSxJj5E5Zm2tFZa13h0xIKehuXGbAqHpC0Q7uZAgMBAAGjEzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAExG2xlV9iubyV0fl0SbsEBGO56Jel+QabvX3TOy5joKBp0kw8MN8YjWxAV6Er5QTi/qXJf9ckwu9nGnmX1bSmkwZ9Ep0VgslTVVsZ9h1HQt/VcT9bxSTFeh0KMvktQ+RlewOjCJSTfnOOTrGYgzdfVVZXvL+xUkey/DEAmLkg+ilBSoxy8hOHIq/OG1l15PuNOf5UmVOpH4HJXvexGMrpRDeXNpIb/Dfv4RlCIqStPdty1CFpoZBaaMasoOk3Rrp5g5zzysWwrHMYr0ZWNQAzQZ6OSWfsKo5qssY8Rpxbl/WNk/zpAO95ClgccpSGAdkWgMs7M565vs6vUmOiQWvwA=</ds:X509Certificate>
            </ds:X509Data>
        </ds:KeyInfo>
    </ds:Signature>
    <samlp:Status>
        <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
    </samlp:Status>
    <saml:Assertion ID="DUO_8af9692e4fdf32993dfacd15286a22c85c352c43" IssueInstant="2024-05-18T18:05:41Z" Version="2.0">
        <saml:Issuer>https://sso-2781261d.sso.duosecurity.com/saml2/sp/DIY0T3WG6QRVD2U1SI1F/metadata</saml:Issuer>
        <ds:Signature>
            <ds:SignedInfo>
                <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
                <ds:Reference URI="#DUO_8af9692e4fdf32993dfacd15286a22c85c352c43">
                    <ds:Transforms>
                        <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
                        <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                    </ds:Transforms>
                    <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
                    <ds:DigestValue>cXP/SDoDRZG4CaAqqHcVq8+Canpa/3PknTJTMT+PmtA=</ds:DigestValue>
                </ds:Reference>
            </ds:SignedInfo>
            <ds:SignatureValue>xxxxxx==</ds:SignatureValue>
            <ds:KeyInfo>
                <ds:X509Data>
                    <ds:X509Certificate>MIIDDTCCAfWgAwIBAgIUP1HDN7PUbmwWMl+nPxKrvWaYhHkwDQYJKoZIhvcNAQELBQAwNjEVMBMGA1UECgwMRHVvIFNlY3VyaXR5MR0wGwYDVQQDDBRESVkwVDNXRzZRUlZEMlUxU0kxRjAeFw0yNDA0MjMxMDUzMjdaFw0zODAxMTkwMzE0MDdaMDYxFTATBgNVBAoMDER1byBTZWN1cml0eTEdMBsGA1UEAwwURElZMFQzV0c2UVJWRDJVMVNJMUYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzVye7WgL2ntwoDt3aPp+0VNGLWDve1KQAPc6u+qAsMqszTULudNHeEUDcBru8EQjahN4K7ID5CstOKmuy70Ob5grzC+bldQcYpcYhNqEfy/5Ycv91U6jeCqWEiNuBxxWlA63rVdfWJ3TF6Rh52Phd76f008afN9GZAsgajik/7jxakofHHDSNGZTnvv65kJz+YglE8byEswKuzPv6oioaVGgF5oI9yRoFi6vSA1o/t74nBfd4BzV8AWUD3065D6mHYOSABFimlnNqRno2TT43PHsgszpJUE/RxxstDlp13FfXdQMSxJj5E5Zm2tFZa13h0xIKehuXGbAqHpC0Q7uZAgMBAAGjEzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAExG2xlV9iubyV0fl0SbsEBGO56Jel+QabvX3TOy5joKBp0kw8MN8YjWxAV6Er5QTi/qXJf9ckwu9nGnmX1bSmkwZ9Ep0VgslTVVsZ9h1HQt/VcT9bxSTFeh0KMvktQ+RlewOjCJSTfnOOTrGYgzdfVVZXvL+xUkey/DEAmLkg+ilBSoxy8hOHIq/OG1l15PuNOf5UmVOpH4HJXvexGMrpRDeXNpIb/Dfv4RlCIqStPdty1CFpoZBaaMasoOk3Rrp5g5zzysWwrHMYr0ZWNQAzQZ6OSWfsKo5qssY8Rpxbl/WNk/zpAO95ClgccpSGAdkWgMs7M565vs6vUmOiQWvwA=</ds:X509Certificate>
                </ds:X509Data>
            </ds:KeyInfo>
        </ds:Signature>
        <saml:Subject>
            <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">example@opw.ie</saml:NameID>
            <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
                <saml:SubjectConfirmationData NotOnOrAfter="2024-05-18T18:10:41Z" Recipient="https://cognito.policlear.org/saml2/idpresponse" InResponseTo="_6bd701a4-f3dc-46fc-899a-003a2782cbea"/>
            </saml:SubjectConfirmation>
        </saml:Subject>
        <saml:Conditions NotBefore="2024-05-18T18:05:11Z" NotOnOrAfter="2024-05-18T18:10:41Z">
            <saml:AudienceRestriction>
                <saml:Audience>urn:amazon:cognito:sp:eu-west-1_kqYKuz6Aj</saml:Audience>
            </saml:AudienceRestriction>
        </saml:Conditions>
        <saml:AuthnStatement AuthnInstant="2024-05-18T18:05:41Z" SessionIndex="DUO_8af9692e4fdf32993dfacd15286a22c85c352c43">
            <saml:AuthnContext>
                <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>
            </saml:AuthnContext>
        </saml:AuthnStatement>
        <saml:AttributeStatement>
            <saml:Attribute Name="app.email" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
                <saml:AttributeValue xsi:type="xs:string">example@opw.ie</saml:AttributeValue>
            </saml:Attribute>
        </saml:AttributeStatement>
    </saml:Assertion>
</samlp:Response>