联合身份标识
使用 WIF 实现 ASP.NET 被动身份验证
Michele Leroux Leroux
联合安全的目标是提供在域间建立信任关系的机制,这样,用户通过自己所属域的身份验证后,就获得了授权可以访问其他域的应用程序和服务。这使单一登录这样的身份验证方法成为可能,采用这种方法,无需针对多个应用程序和域为用户配置和管理重复帐户,从而显著降低将应用程序扩展到受信任方的成本。
在联合安全模型中,身份标识提供程序 (IdP) 执行身份验证,并通过安全令牌服务 (STS) 颁发安全令牌。这些令牌断言有关经过身份验证的用户的信息:用户身份标识和其他可能的信息(包括角色和更细粒度的访问权限)。在联合技术领域,这些信息称为声明,基于声明的访问控制是联合安全模型的核心。在这一模型中,应用程序和服务根据来自受信任颁发机构 (STS) 的声明,对特性和功能的访问进行授权。
Windows Identity Foundation (WIF) 等平台工具大大降低了支持这种联合身份验证的难度。WIF 是一个身份标识模型框架,用于构建基于声明的应用程序和服务,支持基于 SOAP(主动)和基于浏览器(被动)的联合方案。在 MSDN 杂志 2009 年 11 月刊中的文章“通过 WIF 实现基于声明的授权”中,我重点介绍了如何在 Windows Communication Foundation (WCF) 中使用 WIF。在这篇文章中,我介绍了如何为 WCF 服务实现基于声明的安全模型,以及如何迁移到联合身份验证。
在本后续文章中,我将重点介绍被动联合。我将说明被动联合的通信流程,介绍几种在 ASP.NET 应用程序中支持联合的方法,讨论基于声明的 ASP.NET 授权方法,然后介绍单一登录和单一注销方案。同时,我将介绍支持被动联合方案的基础 WIF 功能和组件。
被动联合基础
被动联合方案基于 WS-Federation 规范。该规范规定了如何请求安全令牌,如何发布和获取联合元数据文档,从而简化建立信任关系的过程。WS-Federation 还规定了单一登录和注销过程,以及其他联合实现概念。
WS-Federation 讨论了有关联合的诸多细节,其中有一部分专门介绍基于浏览器的联合,这种联合依靠 HTTP GET 和 POST、浏览器重定向以及 Cookie 实现目标。
被动联合消息传递的某些方面主要基于 WS-Trust 规范。例如,在请求 STS 的安全令牌时,被动联合采用与浏览器兼容的“请求安全令牌”(RST) 和“RST 响应”(RSTR) 形式。在被动联合方案中,我将 RST 称为登录请求消息,将 RSTR 称为登录响应消息。WS-Trust 规范侧重于基于 SOAP(主动)的联合,如 Windows 客户端与 WCF 服务的联合。
图 1 是一个简单的被动联合方案。
图 1 简单被动联合方案
用户在自己的域通过身份验证,根据其角色获得 Web 应用程序访问权限。此身份验证方案中的参与者包括用户(主体)、Web 浏览器(请求者)、ASP.NET 应用程序(依赖方,即 RP)、负责在用户域中对用户进行身份验证的 IdP,以及属于用户域的 STS (IP-STS)。一系列的浏览器重定向操作可以确保用户在访问 RP 之前在自己的域内通过身份验证。
用户浏览至 RP 应用程序 (1),然后重定向到用户的 IdP 进行身份验证 (2)。如果用户尚未在 IdP 经过身份验证,IP-STS 可能进行质询,或将用户重定向到登录页面以收集凭据 (3)。用户提供自己的凭据 (4),然后由 IP-STS (5) 进行身份验证。此时,IP-STS 根据登录请求颁发安全令牌,包含该令牌的登录响应通过浏览器重定向发送到 RP (6)。RP 处理安全令牌,根据令牌携带的声明授予访问权限 (7)。如果成功授权,则向用户显示其最初请求的页面,并且返回会话 Cookie (8)。
使用 WIF 和 ASP.NET 实现此被动联合方案只需要几个步骤:
- 建立 RP 和 IdP (IP-STS) 之间的信任关系
- 对 ASP.NET 应用程序启用被动联合
- 实现授权检查,以控制对应用程序功能的访问。在后面几节中,我将讨论支持被动联合的 WIF 功能,演示配置此简单方案的步骤,然后讨论此方案和其他方案在实践中应注意的事项。
支持被动联合的 WIF 功能
在讨论实现之前,我们回顾一下 WIF 专用于在 ASP.NET 应用程序中支持联合身份验证的功能。首先,WIF 提供以下有用的 HTTP 模块:
- WSFederationAuthenticationModule (FAM): 支持基于浏览器的联合,重定向到适当的 STS 以进行身份验证和令牌颁发,处理生成的登录响应,将已颁发安全令牌创建为 ClaimsPrincipal 以进行授权。此模块还处理其他重要的联合消息,如注销请求。
- SessionAuthenticationModule (SAM): 通过生成包含 ClaimsPrincipal 的会话安全令牌,管理经过身份验证的会话,将会话安全令牌写入 Cookie,管理会话 Cookie 的生存期,如果 Cookie 已存在,则根据 Cookie 重建 ClaimsPrincipal。此模块还包含一个本地会话令牌缓存。
- ClaimsAuthorizatonModule: 提供一个可扩展点,以便安装自定义 ClaimsAuthorizationManager,后者对于集中访问检查非常有用。
- ClaimsPrincipalHttpModule: 根据附加到请求线程的当前用户身份创建 ClaimsPrincipal。另外,该模块还提供一个可扩展点来安装自定义 ClaimsAuthenticationManager,后者用于自定义将附加到请求线程的 ClaimsPrincipal。
ClaimsPrincipalHttpModule 最适合不使用被动联合的应用程序。您可将该模块看作一个有用的工具,在 ASP.NET 应用程序迁移到被动联合之前,它可用来在应用程序内实现基于声明的安全模型。我在以前的文章中讨论过这一 WCF 方法。
其他三种模块通常一起用于被动联合 — 不过 ClaimsAuthorizationModule 是可选的。图 2 演示这些核心模块如何构成请求管道,以及这些模块在典型的联合身份验证请求中的功能。
图 2 被动联合采用的 WIF 组件和 HTTP 模块
请注意图 1 中的被动联合流程,当用户首先浏览至 RP 中的受保护页面时 (1),对应用程序的访问将被拒绝。FAM 处理还未授权的请求,生成登录消息,然后将用户重定向到 IP-STS (2)。IP-STS 验证用户身份 (3),生成登录响应(包含颁发的安全令牌),然后重定向回 RP 应用程序 (4)。
FAM 处理登录响应(确保响应包含经过身份验证的用户的有效安全令牌),根据登录响应创建 ClaimsPrincipal (5)。这将为请求线程和 HttpContext 设置安全主体。然后,FAM 使用 SAM 将 ClaimsPrincipal 序列化为 HTTP Cookie (6),在浏览器会话期间用于后续请求。如果安装了 ClaimsAuthorizationModule,该模块将调用经过配置的 ClaimsAuthorizationManager,以便在访问请求的资源之前针对 ClaimsPrincipal 执行全局访问检查 (7)。
只要请求的资源存在,就可以实现访问控制,这一过程需要使用传统的 ASP.NET 登录控件、IsInRole 检查和其他查询用户声明的自定义代码 (8)。
进行后续请求时,将使用该会话令牌和 SAM 之前写入的 Cookie (9)。这时,SAM 会验证会话令牌,根据令牌重新创建 ClaimsPrincipal (10)。仅当请求是登录响应、注销请求,或者请求被拒绝时(未提供会话令牌或者会话令牌已失效时,可能发生),FAM 才进行验证。
除上述模块之外,被动联合中还有两个很有用的 ASP.NET 控件:
- FederatedPassiveSignIn 控件: 在以下情况下可替代 FAM:如果应用程序将所有未经授权的调用重定向到登录页面,而登录页面只在需要身份验证时才承载此控件。这里假定用户将与登录过程进行交互,该过程在逐级身份验证方案中很有用,在这类方案中,将提示用户提供凭据(可能是除了原始登录凭据之外,应用程序要求的其他凭据)。该控件处理到 STS 的重定向,处理登录响应,根据响应初始化 ClaimsPrincipal,并利用 FAM 和 SAM 公开的功能建立安全会话。
- FederatedPassiveSignInStatus 控件: 此控件提供交互方式,供用户登录 RP 应用程序或从中注销,并且支持联合注销。
图 3 演示采用 FederatedPassiveSignIn 控件时通信流程是如何变化的。应用程序使用窗体身份验证来保护资源,并重定向到承载该控件的登录页面 (1)。用户单击 FederatedPassiveSignIn 控件(或自动重定向到该控件),会触发到 STS 的重定向 (2)。控件页面从 STS 接收响应,通过 FAM 和 SAM 处理登录响应 (3),创建 ClaimsPrincipal,然后写入会话 Cookie (4)。当用户重定向到最初请求的页面时 (5),SAM 验证会话 Cookie,并为登录请求创建 ClaimsPrincipal。此时,ClaimsAuthorizationModule 和该页面可以执行授权检查,如图 2 所示。
图 3 使用 FederatedPassive-SignIn 控件的被动联合
FAM 和 SAM 都通过适当的 SecurityTokenHandler 类型处理传入令牌。登录响应到达时,FAM 循环访问 SecurityTokenHandlerCollection,查找正确的令牌处理程序来读取 XML 令牌。在联合方案中,这个令牌处理程序通常是 Saml11SecurityTokenHandler 或 Saml2SecurityTokenHandler,通过添加自定义令牌处理程序,您也可以采用其他令牌格式。SAM 使用 SessionSecurityTokenHandler 处理与会话 Cookie 相关联的会话令牌。
对于被动联合流程而言,某些身份标识模型配置设置十分重要,这些设置用于初始化 FAM、SAM 和 FederatedPassiveSignIn 控件(当然,后者也会公开可在 Visual Studio 设计器中配置的属性)。您可以以编程方式提供 Microsoft.IdentityModel.Configuration 命名空间的 ServiceConfiguration 类型的实例,也可以在 <microsoft.identityModel> 节中提供声明性配置。图 4 总结了身份标识模型设置,本文后面几部分将对其中很多设置进行介绍。
图 4 基本 <microsoft.identityModel> 元素汇总
节 | 说明 |
<issuerNameRegistry> | 指定受信任证书颁发机构的列表。此列表主要用于验证令牌签名,以便拒绝不受信任的证书所签名的令牌。 |
<audienceUris> | 指定传入 SAML 令牌的有效访问者 URI 的列表。可通过禁用此项允许所有 URI,但不建议这样做。 |
<securityTokenHandlers> | 自定义令牌处理程序的配置设置,或提供自定义令牌处理程序,用于控制对令牌进行验证、身份验证和序列化的方式。 |
<maximumClockSkew> | 调整令牌和应用程序服务器之间的允许时间差,以进行令牌验证。默认偏差时间为 5 分钟。 |
<certificateValidation> | 控制证书的验证方式。 |
<serviceCertificate> | 提供一个服务证书,用于对传入令牌进行解密。 |
<claimsAuthenticationManager> | 提供一个自定义 ClaimsAuthenticationManager 类型,用于自定义或替换要附加到请求线程的 IClaimsPrincipal 类型。 |
<claimsAuthorizationManager> | 提供一个自定义 ClaimsAuthorizationManager 类型,用于从中心组件控制对功能的访问。 |
<federatedAuthentication> | 提供特定于被动联合的设置。 |
启用被动联合
WIF 简化了为 ASP.NET 应用程序配置被动联合的过程。STS 应提供联合元数据(按照 WS-Federation 规范中的说明),WIF 提供联合实用工具 (FedUtil.exe),该工具使用联合元数据建立 RP 和 STS 之间的信任(以及对主动和被动联合方案都适用的其他功能)。通过在 Visual Studio 中右键单击 RP 项目,然后选择“添加 STS 引用”,或者通过命令行,都可以调用 FedUtil。
您将使用 FedUtil 向导完成以下简单步骤:
- 在向导的第一页上,确认向导要修改的配置文件和 RP 应用程序 URI。
- 在第二页上,指定将与 RP 建立信任关系的 STS 的联合元数据 XML 文档路径。
- 在第三页上,提供用于解密令牌的证书。
- 最后一页是 STS 提供的声明的列表,举例来说,利用这个列表,您可以规划访问控制决策。
完成上述向导步骤后,FedUtil 会修改项目,以便添加一个对 Microsoft.IdentityModel 程序集的引用。它还会修改 web.config,以便安装 FAM 和 SAM 模块,并为这些模块提供身份标识模型配置设置。现在,应用程序就支持被动联合了,会将未经授权的请求重定向到受信任的 STS。
这里存在一个假设,即 STS 事先知道 RP(因此会为尝试访问 RP 的经过身份验证的用户颁发令牌),并且具有 RP 要求 STS 在加密令牌时使用的公钥。这是对 ASP.NET 应用程序进行初步联合设置的简便方法。当然,这有助于理解如何在需要调整时从头开始进行设置,以及如何更改向导启用的基本设置。从现在起,我将重点介绍“从头开始设置”方法。
如果不使用 FedUtil,您需要手动添加一个对 Microsoft.IdentityModel 程序集的引用,然后手动配置 FAM 和 SAM 模块以及必要的身份标识模型设置。HTTP 模块应添加到两个部分:Internet Information Services (IIS) 6 的 system.web 和 IIS 7 的 system.webServer。假设应用程序承载于 IIS 7 中,WIF 模块的配置如下:
<modules>
<!--other modules-->
<add name="SessionAuthenticationModule"
type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
preCondition="managedHandler" />
<add name="WSFederationAuthenticationModule"
type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
preCondition="managedHandler" />
</modules>
默认情况下,此配置仅保护扩展名进行了显式映射,以便由 ASP.NET 管道处理的资源(.aspx、.asax 等等)。 为了保护联合身份验证的其他资源,应将这些扩展名映射到 IIS 中的 ASP.NET 管道,或在模块设置中,将 runAllManagedModulesForAllRequests 设置为 true(只适用于 IIS 7),如下所示:
<modules runAllManagedModulesForAllRequests="true">
为启动 FAM,您还必须将 ASP.NET 身份验证模式设置为 None,拒绝匿名用户访问应用程序资源:
<authentication mode="None" />
<authorization>
<deny users="?" />
</authorization>
两种模块都使用图 4 所示的身份标识模型配置设置,典型示例如图 5 所示。 这些设置大多数是由 FedUtil 生成的,但 certificateValidation 设置和 federatedAuthentication 中的某些设置除外。 我通常推荐使用 PeerTrust 证书验证模式,该模式需要将所有受信任证书(包括受信任颁发机构的证书)显式添加到本地计算机的 TrustedPeople 存储。
图 5 被动联合的身份标识模型配置
<microsoft.identityModel>
<service>
<issuerNameRegistry type="Microsoft.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
<trustedIssuers>
<add thumbprint="EF38A0A6D1274766093D3D78BFE4ECA77C62D5C3"
name="http://localhost:60768/STS/" />
</trustedIssuers>
</issuerNameRegistry>
<certificateValidation certificateValidationMode="PeerTrust"
revocationMode="Online" trustedStoreLocation="LocalMachine"/>
<audienceUris>
<add value="http://localhost:50652/ClaimsAwareWebSite2/" />
</audienceUris>
<federatedAuthentication>
<wsFederation passiveRedirectEnabled="true"
issuer="http://localhost:60768/STS/"
realm="http://localhost:50652/ClaimsAwareWebSite2/"
requireHttps="true" />
<cookieHandler requireSsl="true" name="FedAuth"
hideFromScript="true" path="/ClaimsAwareWebSite2" />
</federatedAuthentication>
<serviceCertificate>
<certificateReference x509FindType="FindByThumbprint"
findValue="8A90354199D284FEDCBCBF1BBA81BA82F80690F2"
storeLocation="LocalMachine" storeName="My" />
</serviceCertificate>
</service>
</microsoft.identityModel>
被动联合中通常需要 HTTPS/SSL 来保护颁发的持有者令牌免受中间人的攻击,并且需要对会话 Cookie 使用 HTTPS/SSL。默认情况下,Cookie 对脚本是隐藏的,但它是一项重要设置,所以我在图 5 中对它进行了设置。
至于 Cookie 的名称和路径,名称默认为 FedAuth,路径为应用程序目录。为 Cookie 指定唯一名称可能很有用,尤其是在解决方案中很多 RP 应用程序共享同一个域的情况下。相反,如果希望同一个域中多个应用程序共享 Cookie,您可以选择指定一个通用路径。
在采用 FAM 和 SAM 的被动联合中,通常使用 FedUtil 配置 ASP.NET 应用程序,然后根据解决方案的要求调整相应设置。您也可以使用 PassiveFederationSignIn 控件来代替 FAM,如图 3 所示。您可以在 microsoft.identityModel 节中加载该控件的设置,也可以直接设置控件属性。
如果希望将未经授权的请求重定向到登录页面(用户可以在此通过单击该控件进行显式登录)而不是由 FAM 自动重定向到 STS,控件方法会很有用。例如,如果用户属于多个身份标识提供程序(本地域),登录页面可能会提供一种机制,供用户在重定向到 STS 之前指示其本地域。我稍后会讨论本地域识别。
被动令牌颁发
如前所述,被动联合使用 HTTP GET、POST 和浏览器重定向,实现 RP 与 STS 之间的通信。图 6 演示这一过程中登录请求和登录响应中涉及的主要请求参数。
图 6 被动联合请求中涉及的主要登录请求和响应参数
STS 接收到登录请求时,会根据它的已知 RP 域列表检查 wtrealm 参数,验证该 RP 是不是已知的。STS 可能已有该 RP 的知识,即加密令牌所需的证书,以及应包含在已颁发令牌中的声明。如果该 RP 通过完整登录请求提供可选的 wreq 参数,则可以指示需要哪些声明,STS 可以选择根据这个列表,也可以根据经过身份验证的用户自主决定对哪些声明授权。
在图 1 所示的简单联合方案中,只有一个 RP 和一个 IP-STS 负责用户身份验证。如果 IP-STS 针对 Windows 域进行用户身份验证,它可能颁发角色声明,如管理员、用户或来宾。前提是这些角色对于 RP 授权是有意义的。在下一部分中,我假设这些角色是有意义的,然后讨论授权方法。在这之后,我将讨论 RP 声明转换,根据需要将 STS 声明转换为对于授权更加有用的内容。
基于声明的授权
我在以前的文章中提到过,.NET Framework 中基于角色的安全机制要求安全主体附加到每个线程。安全主体(基于 IPrincipal)在 IIdentity 实现中包装经过身份验证的用户的标识。WIF 根据 IClaimsPrincipal 和 IClaimsIdentity(最终由 IPrincipal 和 IIdentity 派生)提供 ClaimsPrincipal 和 ClaimsIdentity 类型。FAM 处理登录响应时,会为已颁发安全令牌创建 ClaimsPrincipal。同样,SAM 也为会话 Cookie 创建 ClaimsPrincipal。此 ClaimsPrincipal 是 ASP.NET 应用程序进行 WIF 授权的核心。
您可以使用以下任何方法进行授权:
- 使用位置特定的授权设置限制对目录或各应用程序资源的访问。
- 使用 ASP.NET 登录控件(如 LoginView 控件)控制对功能的访问。
- 使用 ClaimsPrincipal 执行动态 IsInRole 检查(如动态隐藏或显示 UI 元素)。
- 使用 PrincipalPermission 类型或 PrincipalPermissionAttribute(如果声明性权限请求看起来适用于某种特定方法)执行动态权限请求。
- 提供自定义 ClaimsAuthorizationManager,将访问权限检查集中在一个组件中,即使在加载请求的资源之前也不例外。
在上述方法中,前三个需要使用 ClaimsPrincipal 类型公开的 IsInRole 方法。您必须选择一个适合于 IsInRole 检查的角色声明类型,以便使用正确的声明控制访问。WIF 的默认角色声明类型为:
https://schemas.microsoft.com/ws/2008/06/identity/claims/role
如果 ClaimsPrincipal 包含定义的声明,则角色声明类型将与默认值匹配。 稍后,我将讨论声明转换上下文中的权限声明。 使用这些声明时,应将权限声明类型指定为角色声明类型,以便使 IsInRole 生效。
您可以使用 web.config 文件对访问特定页面或特定目录进行全局控制。 在应用程序根目录中提供一个位置标记,指定要保护的路径,允许可接受的角色进行访问,拒绝所有其他用户的访问。 下面的代码只允许 Administrators 访问 AdminOnly 目录下的文件:
<location path="AdminOnly">
<system.web>
<authorization>
<allow roles="Administrators" />
<deny users="*"/>
</authorization>
</system.web>
</location>
此外,您还可以在任何子目录中放置 web.config,用于指定授权规则。 将下面的配置放置到 AdminOnly 目录中,结果是一样的:
<configuration>
<system.web>
<authorization >
<allow roles="Administrators" />
<deny users="*"/>
</authorization>
</system.web>
</configuration>
要动态隐藏或显示 UI 组件或控制对页面内功能的访问,可以使用基于角色的控件功能(如 LoginView)。 但是,大多数开发人员更愿意在页面加载过程中显式设置控件的访问控制属性,以获得更精细的控制。 为此,您可以调用 ClaimsPrincipal 公开的 IsInRole 方法。 您可以通过 Thread.CurrentPrincipal 静态属性访问当前主体,如下所示:
if (!Thread.CurrentPrincipal.IsInRole("Administrators"))
throw new SecurityException("Access is denied.");
除了在运行时显式检查 IsInRole 之外,还可以使用 PrincipalPermission 类型写入传统的基于角色的权限要求。 还可以使用所要求的角色声明(第二个构造函数参数)初始化该类型,当调用 Demand 时,将调用当前主体的 IsInRole 方法。 如果找不到该声明,则引发异常:
PrincipalPermission p =
new PrincipalPermission("", "Administrators");
p.Demand();
如果要在相应角色不存在时拒绝请求并引发异常,此方法很有用。
集中处理所有被请求资源的通用权限检查也是很有用的。 有时,如果有访问控制策略(如存储在数据库中的规则),则可以使用中心组件读取这些规则,从而控制对特性和功能的访问。 为此,WIF 提供了一个可以扩展的 ClaimsAuthorizationManager 组件。 我以前的文章中提到过,您可以在身份标识模型节配置这种自定义组件类型:
<microsoft.identityModel>
<service>
<!--other settings-->
<claimsAuthorizationManager
type="CustomClaimsAuthorizationManager"/>
</service>
</microsoft.identityModel>
图 7 演示一个自定义的 ClaimsAuthorizationManager,它验证名称声明是否存在,AdminsOnly 目录内的被请求资源是否需要 Administrators 角色声明。
图 7 自定义 ClaimsAuthorizationManager 实现
public class CustomClaimsAuthorizationManager:
ClaimsAuthorizationManager {
public CustomClaimsAuthorizationManager()
{ }
public override bool CheckAccess(
AuthorizationContext context) {
ClaimsIdentity claimsIdentity =
context.Principal.Identity as ClaimsIdentity;
if (claimsIdentity.Claims.Where(
x => x.ClaimType == ClaimTypes.Name).Count() <= 0)
throw new SecurityException("Access is denied.");
IEnumerable<Claim> resourceClaims =
context.Resource.Where(x=>x.ClaimType==ClaimTypes.Name);
if (resourceClaims.Count() > 0) {
foreach (Claim c in resourceClaims) {
if (c.Value.Contains("\AdminOnly") &&
!context.Principal.IsInRole("Administrators"))
throw new SecurityException("Access is denied.");
}
}
return true;
}
}
CustomClaimsAuthorizationManager 通过重写 CheckAccess 实现这一功能。 此方法提供一个 AuthorizationContext 参数,该参数提供有关如下内容的信息:请求操作(对于被动联合来说,是 HTTP 谓词,如 GET 或 POST)、请求的资源(一个 URI)和尚未附加到请求线程的 ClaimsPrincipal。
声明转换
通常,IP-STS 发出的声明对描述经过身份验证的用户是有用的,但与 RP 的授权要求无关。 IdP 的任务不是了解每个 RP 的授权需要哪种类型的角色、权限或其他更细粒度的项, 而是授权给身份标识提供程序域有关的声明,或 IdP 可针对经过身份验证的用户断言的声明。
因此,RP 可能需要将声明从 IP-STS 转换为与授权更加相关的项。 这表明 RP 可以将用户标识映射(可能按用户名称或 UPN)到 RP 声明集。 假设 IP-STS 授权给默认角色声明,图 8 列出了 RP 按照每个传入角色声明发出的可能权限声明集。 权限声明类型可以是 RP 定义的自定义声明类型,如:
urn:ClaimsAwareWebSite/2010/01/claims/permission
自定义 ClaimsAuthenticationManager 很适合转换传入 IP-STS 声明。 将以下代码添加到 microsoft.identityModel 节,可以安装自定义 ClaimsAuthenticationManager:
<microsoft.identityModel>
<service>
<!--other settings-->
<claimsAuthenticationManager
type="CustomClaimsAuthenticationManager"/>
</service>
</microsoft.identityModel>
图 9 是一个示例 CustomClaimsAuthenticationManager,它将 IP-STS 授权的传入角色声明转换为与 RP 相关的权限声明。
图 8 将角色声明转换为 RP 的权限声明
角色声明 | 权限声明 |
Administrators | Create、Read、Update、Delete |
Users | Create、Read、Update |
Guest | Read |
图 9 RP 自定义声明转换
public class CustomClaimsAuthenticationManager:
ClaimsAuthenticationManager {
public CustomClaimsAuthenticationManager() { }
public override IClaimsPrincipal Authenticate(
string resourceName, IClaimsPrincipal incomingPrincipal) {
IClaimsPrincipal cp = incomingPrincipal;
ClaimsIdentityCollection claimsIds =
new ClaimsIdentityCollection();
if (incomingPrincipal != null &&
incomingPrincipal.Identity.IsAuthenticated == true) {
ClaimsIdentity newClaimsId = new ClaimsIdentity(
"CustomClaimsAuthenticationManager", ClaimTypes.Name,
"urn:ClaimsAwareWebSite/2010/01/claims/permission");
ClaimsIdentity claimsId =
incomingPrincipal.Identity as ClaimsIdentity;
foreach (Claim c in claimsId.Claims)
newClaimsId.Claims.Add(new Claim(
c.ClaimType, c.Value, c.ValueType,
"CustomClaimsAuthenticationManager", c.Issuer));
if (incomingPrincipal.IsInRole("Administrators")) {
newClaimsId.Claims.Add(new Claim(
"urn:ClaimsAwareWebSite/2010/01/claims/permission",
"Create"));
newClaimsId.Claims.Add(new Claim(
"urn:ClaimsAwareWebSite/2010/01/claims/permission",
"Read"));
newClaimsId.Claims.Add(new Claim(
"urn:ClaimsAwareWebSite/2010/01/claims/permission",
"Update"));
newClaimsId.Claims.Add(new Claim(
"urn:ClaimsAwareWebSite/2010/01/claims/permission",
"Delete"));
}
else if (incomingPrincipal.IsInRole("Users")) {
newClaimsId.Claims.Add(new Claim(
"urn:ClaimsAwareWebSite/2010/01/claims/permission",
"Create"));
newClaimsId.Claims.Add(new Claim(
"urn:ClaimsAwareWebSite/2010/01/claims/permission",
"Read"));
newClaimsId.Claims.Add(new Claim(
"urn:ClaimsAwareWebSite/2010/01/claims/permission",
"Update"));
}
else {
newClaimsId.Claims.Add(new Claim(
"urn:ClaimsAwareWebSite/2010/01/claims/permission",
"Read"));
}
claimsIds.Add(newClaimsId);
cp = new ClaimsPrincipal(claimsIds);
}
return cp;
}
}
为实现 IsInRole 检查(如前所述),您必须提供权限声明类型作为角色声明类型。 在图 9 中,这是在构造 ClaimsIdentity 时指定的,因为 RP 要创建 ClaimsIdentity。
如果传入 SAML 令牌是声明的源,则可以向 SecurityTokenHandler 提供角色声明类型。 下面的代码说明如何以声明方式配置 Saml11SecurityTokenHandler,从而将权限声明类型用作角色声明类型:
<microsoft.identityModel>
<service>
<!--other settings-->
<securityTokenHandlers>
<remove type="Microsoft.IdentityModel.Tokens.Saml11.Saml11SecurityTokenHandler, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<add type="Microsoft.IdentityModel.Tokens.Saml11.Saml11SecurityTokenHandler, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
<samlSecurityTokenRequirement >
<roleClaimType
value= "urn:ClaimsAwareWebSite/2010/01/claims/permission"/>
</samlSecurityTokenRequirement>
</add>
</securityTokenHandlers>
</service>
</microsoft.identityModel>
SAML 令牌处理程序包含 samlSecurityTokenRequirement 节,在该节中可以提供名称和角色声明类型的设置,以及与证书验证和 Windows 令牌相关的其他设置。
本地域识别
在前文中,我已重点介绍了包含单个 IP-STS 的简单联合方案。 假设 RP 始终重定向到特定 IP-STS 对用户进行身份验证。
不过,在联合技术领域,RP 可以信任来自多个域的多个令牌颁发机构。 这种情况下,出现了新的挑战,因为 RP 必须确定应由哪个 IP-STS 对请求访问资源的用户进行身份验证。 通过身份验证确定的用户所属的域是用户的主域,因此,这一过程称为主域识别。
应用程序可以使用很多机制进行主域识别:
- 在本示例中,主域是事先已知的,因此,请求始终重定向到特定 IP-STS。
- 用户可以从其他入口浏览至 RP,这样可以提供一个查询字符串,从该入口指示用户的主域。
- RP 可以要求用户登录针对每个主域的特定入口页。 登录页面可以假设一个特定的主域。
- RP 可以根据请求的 IP 地址或其他某种试探法确定主域。
- 如果 RP 无法通过上述方法确定主域,则可以显示一个 UI,供用户选择主域或提供信息帮助 RP 确定主域。
- 如果 RP 支持信息卡,则所选卡可使用主动联合将身份验证驱动至相应的主域。
- WS-Federation 简要规定了如何实现主域识别服务,但没有对此定义完备的规范。
无论如何识别主域,目标都是重定向用户,从而使用正确的 IP-STS 进行身份验证。 有几种可能的方案。 一个方案是,RP 需要动态设置颁发机构 URI,以便将登录请求发送到正确的 IP-STS。 这种情况下,RP 必须在 trustedIssuers 节中列出所有受信任的 IP-STS,如:
<trustedIssuers>
<add thumbprint="6b887123330ae8d26c3e2ea3bb7a489fd609a076"
name="IP1" />
<add thumbprint="d5bf17e2bf84cf2b35a86ea967ebab838d3d0747"
name="IP2" />
</trustedIssuers>
另外,您也可以重写由 FAM 公开的 RedirectingToIdentityProvider 事件,并使用相关试探法为 STS 确定正确的 URI。 为此,可将以下代码添加到 Global.asax 实现中:
void WSFederationAuthenticationModule_RedirectingToIdentityProvider(
object sender, RedirectingToIdentityProviderEventArgs e) {
if (e.SignInRequestMessage.RequestUrl.Contains(
"IP1RealmEntry.aspx")) {
e.SignInRequestMessage.BaseUri =
new Uri("https://localhost/IP1/STS/Default.aspx");
}
else if (e.SignInRequestMessage.RequestUrl.Contains(
"IP2RealmEntry.aspx")) {
e.SignInRequestMessage.BaseUri = new Uri(
"https://localhost/IP2/STS/Default.aspx");
}
}
其他方案需要将主域参数 (whr) 随登录请求一起传递给主 STS。 例如,RP 可能有负责声明转换的资源 STS(R-STS 或 RP-STS)。 RP-STS 不对用户进行身份验证(它不是 IdP),但它与一个或多个其他 IdP 有信任关系。
RP 与 RP-STS 有信任关系,始终认可由 RP-STS 颁发的令牌。 RP-STS 负责将每个请求重定向到正确的 IdP。 RP-STS 可以确定要重定向到的正确 IP-STS(如前面的代码所示),不过 RP 还可以提供有关主域的信息,将主域参数中的这些信息传递到 RP-STS。 这种情况下,RP 动态设置主域参数:
void WSFederationAuthenticationModule_RedirectingToIdentityProvider(
object sender, RedirectingToIdentityProviderEventArgs e) {
if (e.SignInRequestMessage.RequestUrl.Contains(
"IP1RealmEntry.aspx")) {
e.SignInRequestMessage.HomeRealm =
"https://localhost/IP1/STS/Default.aspx";
}
else if (e.SignInRequestMessage.RequestUrl.Contains(
"IP2RealmEntry.aspx")) {
e.SignInRequestMessage.HomeRealm =
"https://localhost/IP2/STS/Default.aspx";
}
}
RP-STS 使用此参数重定向到正确的 IP-STS,然后将声明由 IP-STS 转换为与 RP 相关的声明。
单一登录和单一注销
单一登录和单一注销是联合身份验证的重要组成部分。 通过单一登录功能,用户只要经过一次身份验证,就可以访问多个 RP 应用程序。 顾名思义,单一注销是通过一次请求就从所有 RP 应用程序和所有相关 STS 链中注销。
在图 1 所示的简单联合方案中,用户进行 IP-STS 身份验证,并根据颁发的安全令牌获得 RP 授权。 身份验证结束后,用户有一个 STS 会话 Cookie 和一个 RP 会话 Cookie。 现在,如果该用户浏览至其他 RP,则会重定向到 IP-STS 进行身份验证(假设两个 RP 应用程序都信任同一个 IP-STS)。 因为该用户已经有了一个 IP-STS 会话,所以该 STS 将为第二个 RP 颁发一个令牌,而不提示需要凭据。 现在,用户获得了访问第二个 RP 的权限,并且有第二个 RP 的新会话 Cookie。
前面已经讨论过,WIF 提供 SAM 为经过身份验证的用户写出会话 Cookie。 默认情况下,此会话 Cookie 颁发给域的相对应用程序地址,其基本名称是 FedAuth。 因为联合会话 Cookie 可能较大,所以令牌通常拆分为两个(或更多)Cookie:FedAuth、FedAuth1 等等。
如果同一个域中承载了多个应用程序,在联合方案中,默认行为是,浏览器针对每个 RP 都有一个 FedAuth Cookie(请参阅图 10)。 浏览器只为请求发送与域和路径相关的 Cookie。
图 10 与每个 RP 和 STS 关联的会话 Cookie
通常情况下,都可以使用此默认行为,不过,有时需要针对每个会话 Cookie 提供特定于每个应用程序的唯一名称,尤其是这些应用程序承载于同一个域时。 同一个域中的多个应用程序也可能共享一个会话 Cookie,这种情况下,可以将 Cookie 路径设置为“/”。
如果会话 Cookie 到期,浏览器会从缓存中将它删除,用户将再次被重定向到 STS 进行身份验证。 另外,如果与会话 Cookie 关联的已颁发令牌已到期,WIF 将重定向到 STS 申请新的令牌。
注销更为明确,通常由用户主动执行。 单一注销是 WS-Federation 规范的可选功能,该功能建议 STS 还应通知它颁发过注销请求令牌的其他 RP 应用程序。 这样,会从用户在单一登录会话期间浏览到的所有应用程序中删除会话 Cookie。 在更复杂的包含多个 STS 的方案中,接收到注销请求的主 STS 还应通知其他 STS 执行相同的操作。
为便于讨论,我将着重介绍为实现联合单一注销需要对 RP 执行的操作。 您可以将 FederatedPassiveSignInStatus 控件添加到任何需要支持登录或注销的页面,该控件将自动指示其状态。 登录之后,该控件会显示一个用于注销的链接、按钮或图像。
单击该控件时,它将根据 SignOutAction 属性处理注销,该属性可以是 Refresh、Redirect、RedirectToLoginPage 或 FederatedPassiveSignOut。 如果是前三个属性,则会删除应用程序的会话 Cookie,但不会通知 STS 有关注销请求的信息。 如果选择 FederatedPassiveSignOut 设置,该控件将调用 WSFederationAuthenticationModule 的 SignOut。 这可以确保从应用程序中删除联合会话 Cookie。 另外,注销请求将发送到 STS:
GET https://localhost/IP1/STS?wa=wsignout1.0
如果不使用 FederatedPassiveSignInStatus 控件,则可以直接调用 WSFederationAuthenticationModule.SignOut,以触发将注销请求重定向到 STS 的操作。
单一注销意味着用户从使用联合身份标识登录的所有应用程序注销。 如果 STS 支持此功能,则应保存会话期间用户登录的 RP 应用程序列表,在请求联合注销时向每个 RP 发出清除请求:
GET https://localhost/ClaimsAwareWebSite?wa=wsignoutcleanup1.0
在更复杂的方案中,同样的清除请求应发送到联合会话涉及的所有其他 STS。 为此,STS 必须事先知道每个 RP 和 STS 的清除 URI。 要支持单一注销,RP 应能够处理这些清除请求。 FAM 和 FederatedPassiveSignInStatus 控件都支持此功能。 如果使用 FAM,可将清除请求发送到 RP 的任一 URI,FAM 将处理该请求,清除所有会话 Cookie。 如果使用 FederatedPassiveSignInStatus 控件,则必须将清除请求发送到包含该控件的页面。
事实上,除了建议的查询字符串和通信流程,WS-Federation 规范未详细规定如何实现单一注销和清除行为。 保证单一注销对所有联合合作伙伴有效,这并不容易,但如果您拥有这样的环境,并希望达到此目标,它是确实可行的。
Michele Leroux Bustamante 是 IDesign (idesign.net) 的首席架构师,也是 BiTKOO (bitkoo.com) 的首席安全架构师。她还是 Microsoft 圣地亚哥区域总监和 Microsoft 互联系统 MVP。欢迎访问她的博客 dasblonde.net。
衷心感谢以下技术专家审阅本文:Govind Ramanathan