User.Identity 在 ClaimsIdentity 和 WindowsIdentity 之间波动

User.Identity fluctuates between ClaimsIdentity and WindowsIdentity

我有一个 MVC 站点,允许使用表单登录和 Windows 身份验证登录。我使用一个自定义的 MembershipProvider,它根据 Active Directory 对用户进行身份验证,System.Web.Helpers AntiForgery class 用于 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. 站点收到请求,看到 "use IWA" 选项并重定向到相关操作。已发送 302 响应。
  3. 浏览器自动处理 302 响应并调用重定向目标。
  4. 过滤器发现请求指向 IWA 登录操作并且 User.Identity.IsAuthenticated == false。发送 401 响应。
  5. 浏览器自动处理401响应。如果用户尚未在浏览器中使用 IWA 进行身份验证,他们会收到一个弹出窗口来执行此操作(默认浏览器行为)。收到凭据后,浏览器会使用用户凭据执行相同的请求。
  6. 站点收到经过身份验证的请求并模拟用户以对 Active Directory 执行检查。如果用户通过身份验证,我们使用上面的代码完成登录。
  7. 用户被转发到站点的主页。
  8. 站点收到加载主页的请求。 这就是有时出错的地方
    此时的 User.Identity 是类型 WindowsIdentityAuthenticationType 设置为 Negotiate,并且 NOT 正如我所期望的那样,ClaimsIdentity 在上面的 SignIn 方法中创建。
    该站点通过在视图中调用 @AntiForgery.GetHtml() 为用户准备主页。这样做是为了使用登录用户的详细信息创建一个新的防伪令牌。令牌是使用 WindowsIdentity
  9. 创建的
  10. 随着主页加载,向服务器发出的 ajax 请求随 ClaimsIdentity 一起到达!因此,第一个 POST 请求到达不可避免地会导致 AntiForgeryException,其中它发送的防伪令牌是 "for a different user".

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

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

我觉得我要么遗漏了有关 User.Identity 的内容,要么我在登录过程中做错了什么……有什么想法吗?

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

Use caution when setting this value. Using it improperly can open security vulnerabilities in the application.

仅此而已,我不知道这里实际上打开了哪些安全漏洞,因此我不愿意将其用作解决方案。

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

如 asp.net/Security git 的问题部分所述,NTLM and cookie authentication #1467:

Identities contains both, the windows identity and the cookie identity.

It looks like with ClaimsPrincipals you can set a static Func<IEnumerable<ClaimsIdentity>, ClaimsIdentity> called PrimaryIdentitySelector which you can use in order to select the primary identity to work with.

为此,创建一个带有签名的静态方法:

static ClaimsIdentity MyPrimaryIdentitySelectorFunc(IEnumerable<ClaimsIdentity> identities)

此方法将用于检查 ClaimsIdentity 列表和 select 您喜欢的列表。
然后,在您的 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;
}

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

中所述

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

我不确定这是否有帮助,但这就是我为我解决这个问题的方法。

当我添加 Windows 身份验证时,它在 Windows 和声明身份之间波动。我注意到 GET 请求得到 ClaimsIdentityPOST 请求得到 WindowsIdentity。这非常令人沮丧,我决定调试并将断点设置为 DefaultHttpContext.set_UserIISMiddleware 正在设置 User 属性,然后我注意到它有一个 AutomaticAuthentication,默认值为 true,它设置了 User 属性。我将其更改为 false,因此 HttpContext.User 一直变为 ClaimsPrincipal,欢呼。

现在我的问题变成了如何使用 Windows 身份验证。幸运的是,即使我将 AutomaticAuthentication 设置为 falseIISMiddleware 也会用 WindowsPrincipal 更新 HttpContext.Features,所以 var windowsUser = HttpContext.Features.Get<WindowsPrincipal>(); returns Windows 我的 SSO 页面上的用户。

一切正常,顺利,没有波动,什么都没有。表单基础和 Windows 身份验证一起工作。

services.Configure<IISOptions>(opts =>
{
    opts.AutomaticAuthentication = false;
});