首页 > 解决方案 > User.Identity 在 ClaimsIdentity 和 WindowsIdentity 之间波动

问题描述

我有一个 MVC 站点,它允许使用表单登录和 Windows 身份验证登录。我使用自定义 MembershipProvider 对 Active Directory 的用户进行身份验证,使用 System.Web.Helpers AntiForgery 类进行 CSRF 保护,以及 Owin cookie 身份验证中间件。

在登录期间,一旦用户通过了针对 Active Directory 的身份验证,我将执行以下操作:

IAuthenticationManager authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
authenticationManager.SignOut(StringConstants.ApplicationCookie);
var identity = new ClaimsIdentity(StringConstants.ApplicationCookie,
    ClaimsIdentity.DefaultNameClaimType,
    ClaimsIdentity.DefaultRoleClaimType);
if(HttpContext.Current.User.Identity is WindowsIdentity)
{
    identity.AddClaims(((WindowsIdentity)HttpContext.Current.User.Identity).Claims);
}
else
{
    identity.AddClaim(new Claim(ClaimTypes.Name, userData.Name));
}
identity.AddClaim(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", "Active Directory"));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userData.userGuid));
authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = false }, identity);

我的 SignOut 函数如下所示:

IAuthenticationManager authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
authenticationManager.SignOut(StringConstants.ApplicationCookie);

登录是通过 jQuery.ajax 请求执行的。成功后,将Window.location更新到站点的主页。

使用 Forms 和IntegratedWindowsAuthentication(IWA) 登录都可以,但是使用 IWA 登录时遇到了问题。这就是发生的事情:

  1. 用户在登录页面上选择 IWA 并点击提交按钮。这通过 ajax 请求发送到常规登录操作。
  2. 站点收到请求,看到“使用 IWA”选项并重定向到相关操作。发送 302 响应。
  3. 浏览器自动处理 302 响应并调用重定向目标。
  4. 过滤器看到请求指向 IWA 登录操作,并且 User.Identity.IsAuthenticated == false。发送 401 响应。
  5. 浏览器会自动处理 401 响应。如果用户尚未在浏览器中使用 IWA 进行身份验证,他们会收到一个弹出窗口来执行此操作(默认浏览器行为)。收到凭据后,浏览器会使用用户凭据执行相同的请求。
  6. 该站点接收经过身份验证的请求并模拟用户对 Active Directory 执行检查。如果用户通过身份验证,我们使用上面的代码完成登录。
  7. 用户被转发到站点的主页。
  8. 该站点收到加载主页的请求。这就是事情有时会出错的地方。
    此时User.Identity的类型WindowsIdentity设置AuthenticationTypeNegotiate,而不是我所期望的,在上面的方法中ClaimsIdentity创建。该站点通过调用视图 为用户准备主页。这样做是为了使用登录用户的详细信息创建一个新的 AntiForgery 令牌。令牌是用SignIn
    @AntiForgery.GetHtml()WindowsIdentity
  9. 当主页加载时,向服务器发出的 ajax 请求以ClaimsIdentity! 因此,第一个POST到达的请求不可避免地会导致AntiForgeryException它发送的防伪令牌是“为不同的用户”。

刷新页面会导致主页加载ClaimsIdentity并允许POST请求运行。

第二,相关的问题:在刷新后的任何时候,一旦事情正常工作,POST请求可能会到达WindowsIdentity而不是到达ClaimsIdentity,再次抛出AntiForgeryException.

我觉得我要么User.Identity在登录过程中遗漏了一些东西,要么我做错了什么......有什么想法吗?

注意:设置AntiForgeryConfig.SuppressIdentityHeuristicChecks = true;允许AntiForgery.Validate操作成功,无论是否WindowsIdentity收到ClaimsIdentity,但如 MSDN 所述:

设置此值时要小心。使用不当可能会在应用程序中打开安全漏洞。

没有更多的解释,我不知道这里实际上打开了哪些安全漏洞,因此我不愿意将其用作解决方案。

标签: c#asp.net-mvcowinclaims-based-identitywindows-identity

解决方案


原来问题是 ClaimsPrincipal 支持多个身份。如果您处于具有多个身份的情况,它会自行选择一个。我不知道是什么决定了 IEnumerable 中身份的顺序,但无论它是什么,它显然都必然导致用户会话生命周期中的恒定顺序。

如 asp.net/Security git 的问题部分所述,NTLM 和 cookie 身份验证 #1467

身份包含 Windows 身份和 cookie 身份。

看起来ClaimsPrincipals您可以设置一个static Func<IEnumerable<ClaimsIdentity>, ClaimsIdentity>调用PrimaryIdentitySelector,您可以使用它来选择要使用的主要身份。

为此,请使用签名创建一个静态方法:

static ClaimsIdentity MyPrimaryIdentitySelectorFunc(IEnumerable<ClaimsIdentity> identities)

此方法将用于遍历ClaimsIdentitys 列表并选择您喜欢的一个。
然后,在您的Global.asax.cs中将此方法设置为PrimaryIdentitySelector,如下所示:

System.Security.Claims.ClaimsPrincipal.PrimaryIdentitySelector = MyPrimaryIdentitySelectorFunc;

我的PrimaryIdentitySelector方法最终看起来像这样:

public static ClaimsIdentity PrimaryIdentitySelector(IEnumerable<ClaimsIdentity> identities)
{
    //check for null (the default PIS also does this)
    if (identities == null) throw new ArgumentNullException(nameof(identities));

    //if there is only one, there is no need to check further
    if (identities.Count() == 1) return identities.First();

    //Prefer my cookie identity. I can recognize it by the IdentityProvider
    //claim. This doesn't need to be a unique value, simply one that I know
    //belongs to the cookie identity I created. AntiForgery will use this
    //identity in the anti-CSRF check.
    var primaryIdentity = identities.FirstOrDefault(identity => {
        return identity.Claims.FirstOrDefault(c => {
            return c.Type.Equals(StringConstants.ClaimTypes_IdentityProvider, StringComparison.Ordinal) &&
                   c.Value == StringConstants.Claim_IdentityProvider;
        }) != null;
    });

    //if none found, default to the first identity
    if (primaryIdentity == null) return identities.First();

    return primaryIdentity;
}

[编辑]
现在,这还不够,因为当列表中只有一个时PrimaryIdentitySelector似乎没有运行。这会导致登录页面出现问题,有时浏览器会在加载页面时传递 WindowsIdentity,但不会在登录请求中传递它{exasperated sigh}。为了解决这个问题,我最终为登录页面创建了一个 ClaimsIdentity,然后手动覆盖线程的 Principal,如此 SO question中所述。IdentityIdentities

这会导致 Windows 身份验证出现问题,因为OnAuthenticate不会发送 401 来请求 Windows 身份。要解决问题,您必须注销登录身份。如果登录失败,请确保重新创建登录用户。(您可能还需要重新创建 CSRF 令牌)


推荐阅读