什么是SAML
安全断言标记语言(Security Assertion Markup Language,SAML) 是一种用于安全性断言的标记语言,目前的最新版本是2.0。在SAML协议中,一旦用户身份被主网站(身份提供者)认证过后,该用户再去访问其他在主网站注册过的应用(服务提供者)时,都可以直接登录,而不用再输入用户名和密码。
SAML 主要用作基于网络的身份验证机制,因为它依赖于使用浏览器代理来代理身份验证流程。
SAML规范定义了3个角色:委托人(Principal,通常是用户)、身份提供者(Identity Provider,IDP)和服务提供者(Service Provider,SP)。在SAML协议流程中,委托人向SP请求服务。SP向IDP请求并获得认证断言。基于这个断言,SP可以做出访问控制决策,即可以决定是否为请求的委托人执行服务。
SAML断言
SAML 2.0规范定义了三种断言声明并且每一种都和一个主题相关。详细信息如下。
(1)身份验证(Authentication)断言。该断言的主题是在某个时间通过某种方式被认证。
<saml:AuthnStatement>
(2)属性(Attribute)断言。该断言的主题和用户的某些属性相关联。
<saml:AttributeStatement>
(3)授权决策(Authorization Decision)断言。该断言的主题被允许或禁止访问某个资源。
SAML元数据
元数据是配置数据,包含了关于SAML通信过程中的各方信息,如IDP或SP的ID、Web服务的URL地址、所支持的绑定类型和通信中的密钥等。
为了安全地互操作,合作伙伴以任何形式和任何可能的方式共享元数据,应至少共享以下元数据:实体编号、密钥、协议端点(绑定和位置)。
想要完全自动化元数据共享过程,则需要标准文件格式。为此,SAML 2.0元数据规范定义了SAML元数据的标准表示,它简化了SAML软件的配置过程,并使得创建安全、自动化的元数据共享过程成为可能。
SAML元数据包括:实体ID和实体属性、角色描述符、用户界面元素、签名密钥或加密密钥、单点登录协议端点、注册和出版信息、组织和联系信息(供终端用户使用)。在下面的示例中,元数据中的特定URI(如entityID端点位置)通过URI的域组件映射到责任方。
(1)实体元数据
实体(身份提供者或服务提供者)的所有元数据元素都包含在 元素中,如 urn:oasis:names:tc:SAML:2.0:metadata 名称空间所定义。每个 元素都必须包含一个entityID XML 属性,其值必须是全局唯一的。因此,entityID 必须具有 URL 的语法,该 URL 根植于负有法律责任的组织的 DNS 域。(请注意,虽然entityID 必须具有 URL 的语法,但并不要求它是实际资源的定位器。如果entityID是一个可解析的网络链接,则该链接应指向一个介绍服务的网页,并提及该位置是服务的标识符)。
以下代码示例说明了SAML<md:EntityDescriptor>元素的常见技术特性(下面讲解代码中加粗字体的元素和属性)。
复制 < md : EntityDescriptor entityID = "https://idp.example.org/SAML2" validUntil = "2023-01-01T00:00:00Z" xmlns : md = "urn:oasis:names:tc:SAML:2.0:metadata" >
< md : IDPSSODescriptor WantAuthnRequestsSigned = "true" protocolSupportEnumeration = "urn:oasis:names:tc:SAML:2.0:protocol" >
< md : KeyDescriptor use = "signing" >
< ds : KeyInfo xmlns : ds = "http://www.w3.org/2000/09/xmldsig#" >
< ds : X509Data >
< ds : X509Certificate >MIICizCCAfQCCQCY8tKa...</ ds : X509Certificate >
</ ds : X509Data >
</ ds : KeyInfo >
</ md : KeyDescriptor >
< md : SingleSignOnService Binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location = "https://idp.example.org/sso" />
</ md : IDPSSODescriptor >
< ds : Signature xmlns : ds = "http://www.w3.org/2000/09/xmldsig#" >
<!-- 在这里添加数字签名相关信息 -->
</ ds : Signature >
</ md : EntityDescriptor >
entityID属性: 实体的唯一标识符。entityID是必需项。
<ds:Signature>元素: 包含一个数字签名,用于确保元数据的真实性和完整性。签名者是元数据注册商的受信任的第三方。
<mdrpi:RegistrationInfo>元素: 元数据注册商标识符的扩展信息。
<mdrpi:PublicationInfo>元素: 元数据发布者。其creationInstant属性给出了创建元数据的精确时刻。将creationInstant属性值与validUntil属性值进行比较,可以确定元数据的有效期为两周。
<mdattr:EntityAttributes>元素 :包括一个单一的实体属性。
<md:Organization>元素: 定义组织的实体描述符,包括组织名称、显示名称、组织URL等信息。
<md:ContactPerson>元素 :标识负责该实体的技术人员的联系信息。
(2)IDP元数据
身份提供者元数据 是一个SAML身份提供者管理一个单点登录服务端点,接收来自服务提供者的认证请求。该角色中身份提供者的实体描述符包含一个<md:IDPSSODescriptor>元素,该元素包含一个或多个<md:SingleSignOnService>端点。
复制 < md : EntityDescriptor entityID = "https://idp.example.org/SAML2" validUntil = "2023-01-01T00:00:00Z" xmlns : md = "urn:oasis:names:tc:SAML:2.0:metadata" xmlns : mdui = "urn:oasis:names:tc:SAML:metadata:ui" >
< md : IDPSSODescriptor WantAuthnRequestsSigned = "true" protocolSupportEnumeration = "urn:oasis:names:tc:SAML:2.0:protocol" >
< md : KeyDescriptor use = "signing" >
< ds : KeyInfo xmlns : ds = "http://www.w3.org/2000/09/xmldsig#" >
< ds : X509Data >
< ds : X509Certificate >MIICizCCAfQCCQCY8tKa...</ ds : X509Certificate >
</ ds : X509Data >
</ ds : KeyInfo >
</ md : KeyDescriptor >
< md : SingleSignOnService Binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location = "https://idp.example.org/sso" />
< md : SingleSignOnService Binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location = "https://idp.example.org/sso-post" />
< md : SingleSignOnService Binding = "urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location = "https://idp.example.org/sso-soap" />
</ md : IDPSSODescriptor >
< mdui : UIInfo >
<!-- 在这里添加UI信息 -->
< mdui : DisplayName xml : lang = "en" >Example Service Provider</ mdui : DisplayName >
< mdui : Description xml : lang = "en" >This is an example service provider. </ mdui : Description >
< mdui : Logo height = "80" width = "200" >https://example.com/logo.png</ mdui : Logo >
</ mdui : UIInfo >
</ md : EntityDescriptor >
<md:IDPSSODescriptor>元素描述了身份提供者所处的单点登录服务。有关此元素的详细信息如下。<mdui:UIInfo>元素:包含一组用于在服务提供者处建立动态用户界面语言。服务提供者最重要的用户界面是身份提供者界面(登录页面)。<md:KeyDescriptor use="signing">元素:身份提供者配置有SAML签名私钥,相应的验签公钥包含在其中。在本例中,密钥描述符中省略了密钥相关示例。<md:SingleSignOnService> 元素:一组元素项,包括Binding属性和Location属性。其中,Binding属性是SAML 2.0绑定规范(SAMLBind)中指定的标准URI;Location属性是服务提供者使用身份提供者元数据中的属性值来路由SAML消息,这最大限度地降低了非法身份提供者进行中间人攻击的可能性。
(3)SP元数据
服务提供者元数据是SAML服务提供者管理的断言,用户服务端点从身份提供者处接收认证断言。该角色中的服务提供者的实体描述符包含一个<md:SPSSODescriptor>元素,该元素包含一个或多个<md:AssertionConsumerService>端点。以下代码示例说明了这样的端点(下面讲解代码中加粗字体的元素和属性)。
复制 < EntityDescriptor xmlns = "urn:oasis:names:tc:SAML:2.0:metadata" entityID = "https://sp.example.com" >
< SPSSODescriptor protocolSupportEnumeration = "urn:oasis:names:tc:SAML:2.0:protocol" >
< AssertionConsumerService Binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location = "https://sp.example.com/acs" />
</ SPSSODescriptor >
< mdui : UIInfo >
< mdui : DisplayName xml : lang = "en" >Example Service Provider</ mdui : DisplayName >
< mdui : Description xml : lang = "en" >This is an example service provider.</ mdui : Description >
< mdui : Logo height = "80" width = "200" >https://example.com/logo.png</ mdui : Logo >
</ mdui : UIInfo >
< idpdisc : DiscoveryResponse Binding = "urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol" Location = "https://sp.example.com/discovery" />
< md : KeyDescriptor use = "encryption" >
< KeyInfo xmlns = "http://www.w3.org/2000/09/xmldsig#" >
< X509Data >
< X509Certificate >SP_PUBLIC_ENCRYPTION_CERTIFICATE</ X509Certificate >
</ X509Data >
</ KeyInfo >
< EncryptionMethod Algorithm = "http://www.w3.org/2009/xmlenc11#aes256-gcm" />
</ md : KeyDescriptor >
< md : NameIDFormat >urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</ md : NameIDFormat >
< md : AssertionConsumerService Binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location = "https://sp.example.com/acs" />
</ EntityDescriptor >
SAML验证流程
假设场景如下:
1.John在airline.example.com 使用他的用户名johndoe 预定航班;
2. John 接下来访问 cars.example.co.uk 租车,这个网站发现他之前访问过他们的 IdP 成员网站 airline.example.com .因此 cars.example.co.uk 询问John 是否他愿意授权关联他的 cars.example.co.uk 身份给airline.example.com .
3. John 同意了,浏览器重定向给 airline.example.com , 这个网站创建了一个新的假名/SAML断言, azqu3H7 作为 John‘s 访问 cars.example.co.uk 时侯的用户名,假名与他的 johndoe 账户关联。两个提供商都同意在后续的交互中使用这个身份代表John.
4. John带着SAML断言azqu3H7 重定向给 cars.example.co.uk , 由于这是 cars.example.co.uk 首次看到该标识符,因此它不知道该标识符适用于哪个本地用户账户。
5. 接下来,John还希望预定酒店,访问hotels.example.ca ,浏览器重定向给 airline.example.com 重复刚才的登录流程,创建一个新的假名/SAML断言, f78q9C0
6. John 带着新的SAML断言重定向回 hotels.example.ca SP 。这个SP 后续使用这个断言作为联合名称标识符与 IdP airline.example.com 交互。
流程如下:
比如访问SP的index页面,页面内容要求用户登录后查看。
在服务提供者启动SAML协议流程之前,必须知道浏览器用户的首选身份提供者。服务提供者将使用符合身份提供者发现服务协议和配置文件的本地发现服务。
复制 302 Redirect
Location: https://ds.example.com/idpdise?entitylD=https%3A%2F %2Fsso.example.
org%2Fportal
浏览器用户通过重定向请求发现服务,元数据中的<mdui:UIInfo>用户界面元素可用于构建动态发现界面。
GET /idpdisc? entityID=https%3A%2F%2Fsso.example.org %2Fportal HTTP/1.1
Host: ds.example.com
复制 302 Redirect
Location: https://sp.example.com/SAML2/Login?entityID=https%3A%2F%2Fsso.example.org%2Fidp
浏览器用户通过重定向在服务提供者处请求发现响应端点。
复制 GET /SAML2/Login?entityID=https%3A%2F%2Fsso.example.org%2Fidp HTTP / 1.1
Host : sp.example.com
服务提供者生成相关<samlp:AuthnRequest>元素,在URL查询字符串中编码SAML请求,然后将浏览器用户重定向到身份提供者处的单点登录服务。
复制 302 Redirect
Location : https://idp.example.org/SAML2/SSO/Redirect?SAML&request=request&RelayState=token
浏览器用户通过重定向在身份提供者处请求单点登录服务端点。
复制 GET /SAML2/SSO/Redirect?SAML&request=request&RelayState=token HTTP / 1.1
Host : idp.example.org
身份提供者向用户的浏览器返回一个登录页面。
浏览器用户向身份提供者提交HTML表单。
身份提供者向用户的浏览器返回一个XHTML文档。该文档包含一个以XHTML格式编码的SAML响应。
复制 < form method = "post" action = "https://yourportal.e3learning.com.au/saml/SSO/alias/SiteAlias" >
< input type = "hidden" name = "SAMLResponse" value = "response" />
<!-- 添加额外的SAML响应内容 -->
< input type = "hidden" name = "AdditionalAttribute" value = "additional value" />
< input type = "hidden" name = "RelayState" value = "token" />
...
< input type = "submit" value = "提交" />
</ form >
Service Provider Initiated (SP-initiated)
访问通常从IdP发起,单点登录的portal可以登录到多种应用上。通常只需要IdP登录后就可以访问SP。
https://developer.okta.com/docs/concepts/saml/#understanding-sp-initiated-sign-in-flow
在 SP 发起的流程中,用户尝试直接访问 SP 端受保护的资源,而 IdP 并不知道这一尝试。这就产生了两个问题。首先,如果需要对联合身份进行验证,则需要识别正确的 IdP。对于由 SP 发起的登录,SP 最初对身份一无所知。作为开发人员,你需要弄清楚 SP 如何确定哪个 IdP 应该接收 SAML 请求。在某些情况下,如果你的应用程序 URL 包含映射到唯一租户和 IdP 的子域信息,那么被点击的资源链接就足以识别 IdP。如果情况并非如此,则可能需要提示最终用户提供其他信息,如用户 ID、电子邮件或公司 ID。你需要让 SP 识别试图访问资源的用户属于哪个 IdP。请记住,您只是在提示输入标识符,而不是凭据。
SAML 在设计上是一个异步协议。由 SP 发起的登录流程首先会生成一个 SAML 身份验证请求,该请求会被重定向到 IdP。此时,SP 不会存储任何有关请求的信息。当 IdP 返回 SAML 响应时,SP 对触发身份验证请求的初始深度链接一无所知。幸运的是,SAML 通过一个名为 RelayState 的参数来支持这一点。
但是在AWS,RelayState 是一个 HTTP 参数,在联盟身份验证过程中,relay state会在应用程序内重定向用户。对于 SAML 2.0,该值未经修改就会传递给应用程序。应用程序属性配置完成后,IAM Identity Center 会将relay state连同 SAML 响应一起发送给应用程序。在 AWS 中使用基于 SAML 的身份联合时,您可以使用 RelayState 将已登录并通过身份验证的用户重定向到任何 AWS 控制台页面,比如:
复制 https://eu-west-1.console.aws.amazon.com/ec2/v2/home?region=eu-west-1#Instances:tag:Owner=Alessandro;sort=instanceId
Identity Provider Initiated (IdP-initiated)
在此流程中,从IdP登录后再访问SP。最常用也最推荐的一种访问方式。
证书
SP 需要从 IdP 获取公共证书来验证签名。证书存储在 SP 端,并在 SAML 响应到达时使用。
ACS Endpoint
Assertion Consumer Service URL - 通常简称为 SP 登录 URL。这是 SP 提供的用于发布 SAML 响应的端点。SP 需要将此信息提供给 IdP。
IdP Sign-in URL
这是在 IdP 端发出 SAML 请求的端点。SP 需要从 IdP 获取此信息。
⚠️注意
SP从不直接与IdP互动。浏览器作为代理执行所有的重定向。
SP在知道用户是谁之前需要知道要重定向给哪个IdP。
在IdP返回 SAML 断言之前,SP不知道用户是谁。
这个流程不一定从SP开始。IdP可以启动认证流程。
SAML 验证流程是异步的。SP不知道IdP是否会完成整个流程。因此,SP不会维护任何已生成的身份验证请求的任何状态。当SP收到IdP的响应时,响应必须包含所有必要信息。
SAML验证流程
访问SP,SP发现你没有认证信息,它就会生成一个 SAML 的认证请求数据包,把这个请求放在一个 HTML 的 form 的一个隐藏域中,把这个HTML form返回给你。 这个form后面有一句 javascript 会自动提交这个 form。 而 form 的 action 地址就是提前配置好的 IdP上的一个地址。
复制 < samlp : AuthnRequest
xmlns : samlp = "urn:oasis:names:tc:SAML:2.0:protocol"
xmlns : saml = "urn:oasis:names:tc:SAML:2.0:assertion"
ID = "aaf23196-1773-2113-474a-fe114412ab72"
Version = "2.0"
IssueInstant = "2004-12-05T09:21:59"
AssertionConsumerServiceIndex = "0"
AttributeConsumingServiceIndex = "0" >
< saml : Issuer >https://sp.example.com/SAML2</ saml : Issuer >
< samlp : NameIDPolicy
AllowCreate = "true"
Format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" />
</ samlp : AuthnRequest >
重定向到IdP的登录页面,IdP会要求用户提供用户名密码进行身份验证,IdP 在认证你的身份之后,为你生成一些断言, 证明你是谁,你有什么权限等等,并用自己的私钥签名,然后包装成一个 response 格式,放在 form 里返回给你。
断言类似于下面的XML:
复制 < saml : Assertion
xmlns : saml = "urn:oasis:names:tc:SAML:2.0:assertion"
xmlns : xs = "http://www.w3.org/2001/XMLSchema"
xmlns : xsi = "http://www.w3.org/2001/XMLSchema-instance"
ID = "b07b804c-7c29-ea16-7300-4f3d6f7928ac"
Version = "2.0"
IssueInstant = "2004-12-05T09:22:05" >
< saml : Issuer >https://idp.example.org/SAML2</ saml : Issuer >
< ds : Signature
xmlns : ds = "http://www.w3.org/2000/09/xmldsig#" >...</ ds : Signature >
< saml : Subject >
..........
</ saml : Subject >
< saml : Conditions
.........
</saml:Conditions>
< saml : AuthnStatement
AuthnInstant = "2004-12-05T09:22:00"
SessionIndex = "b07b804c-7c29-ea16-7300-4f3d6f7928ac" >
< saml : AuthnContext >
< saml : AuthnContextClassRef >
urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
</ saml : AuthnContextClassRef >
</ saml : AuthnContext >
</ saml : AuthnStatement >
< saml : AttributeStatement >
< saml : Attribute
xmlns : x500 = "urn:oasis:names:tc:SAML:2.0:profiles:attribute:X500"
x500 : Encoding = "LDAP"
NameFormat = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
Name = "urn:oid:1.3.6.1.4.1.5923.1.1.1.1"
FriendlyName = "eduPersonAffiliation" >
< saml : AttributeValue
xsi : type = "xs:string" >member</ saml : AttributeValue >
< saml : AttributeValue
xsi : type = "xs:string" >staff</ saml : AttributeValue >
</ saml : Attribute >
</ saml : AttributeStatement >
</ saml : Assertion >
Response 语句大概如下:
复制 < samlp : Response
xmlns : samlp = "urn:oasis:names:tc:SAML:2.0:protocol"
xmlns : saml = "urn:oasis:names:tc:SAML:2.0:assertion"
ID = "identifier_2"
InResponseTo = "identifier_1"
Version = "2.0"
IssueInstant = "2004-12-05T09:22:05"
Destination = "https://sp.example.com/SAML2/SSO/POST" >
< saml : Issuer >https://idp.example.org/SAML2</ saml : Issuer >
< samlp : Status >
< samlp : StatusCode
Value = "urn:oasis:names:tc:SAML:2.0:status:Success" />
</ samlp : Status >
< saml : Assertion
xmlns : saml = "urn:oasis:names:tc:SAML:2.0:assertion"
ID = "identifier_3"
Version = "2.0"
IssueInstant = "2004-12-05T09:22:05" >
< saml : Issuer >https://idp.example.org/SAML2</ saml : Issuer >
<!-- a POSTed assertion MUST be signed -->
....................
</ saml : Assertion >
</ samlp : Response >
正如上面第2步一样,它也会把 response 包装在一个 HTML form 里面返回给你,并自动提交给 SP 的某个地址。
SP 读到 form 提交上来的 断言。 并通过 IdP 的公钥验证了断言的签名,于是信任了断言。 知道你是IdP的合法用户。 所以就最终给你返回了你最初请求的页面了。
参考资料
https://developer.okta.com/docs/concepts/saml/
Security Assertion Markup Language (SAML) V2.0 Technical Overview
https://www.samltool.com/generic_sso_res.php