我有一个MVC站点,该站点允许使用Forms登录名和Windows身份验证登录。我使用一个自定义的MembershipProvider,该成员针对Active Directory,用于CSRF保护的System.Web.Helpers AntiForgery类以及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登录时遇到了问题。这是发生了什么:
  • 用户在登录页面上选择IWA,然后单击“提交”按钮。这是通过ajax请求发送到常规登录操作的。
  • 该站点收到请求,看到“使用IWA”选项,并重定向到相关操作。 302响应已发送。
  • 浏览器会自动处理302响应并调用重定向目标。
  • 筛选器可以看到请求已转到IWA登录操作,并且User.Identity.IsAuthenticated == false。发送401响应。
  • 浏览器会自动处理401响应。如果用户尚未在浏览器中使用IWA进行身份验证,则他们会弹出一个窗口来进行身份验证(默认浏览器行为)。收到凭据后,浏览器将使用用户凭据执行相同的请求。
  • 该站点收到经过身份验证的请求,并模拟用户对Active Directory执行检查。如果用户通过身份验证,我们将使用上面的代码最终确定SignIn。
  • 用户将被转发到站点的主页。
  • 该站点收到加载主页的请求。 这是有时出现问题的地方
    此时的User.Identity的类型为WindowsIdentity,且AuthenticationType设置为Negotiate,而不是我期望的那样,它是在上面的ClaimsIdentity方法中创建的SignIn
    该站点通过在 View 中调用@AntiForgery.GetHtml()为用户准备主页。这样做是为了使用登录用户的详细信息创建一个新的AntiForgery token 。 token 是使用WindowsIdentity
  • 创建的
  • 在加载主页时,向服务器发出的ajax请求以ClaimsIdentity到达!因此,第一个到达的POST请求不可避免地会导致AntiForgeryException,在该位置它发送的防伪 token 是“针对其他用户”。

  • 刷新页面会使主页加载ClaimsIdentity并允许POST请求起作用。

    其次,相关的问题:在刷新后的任何时候,只要一切正常运行,POST请求就可以使用WindowsIdentity而不是ClaimsIdentity到达,再次抛出AntiForgeryException
  • 没有任何特定的发布请求
  • 它不是经过任何特定的时间(可能是第一个/第二个请求,可能是百分之一百),
  • 不一定是在该 session 期间第一次调用特定的发布请求。

  • 我觉得我要么缺少有关User.Identity的东西,要么在登录过程中做错了什么……有什么想法吗?

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



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

    最佳答案

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

    正如asp.net/Security git的Issues部分NTLM and cookie authentication #1467所述:







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

    static ClaimsIdentity MyPrimaryIdentitySelectorFunc(IEnumerable<ClaimsIdentity> identities)
    

    该方法将用于遍历ClaimsIdentity的列表并选择您喜欢的方法。
    然后,在 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列表中只有一个Identity时,Identities似乎没有运行。这在登录页面中引起问题,有时浏览器在加载页面时会通过WindowsIdentity,但在登录请求中却不会传递WindowsIdentity {激怒叹气}。为了解决这个问题,我最终为登录页面创建了ClaimsIdentity,然后按照this SO question中的描述手动覆盖了线程的Principal。

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

    10-05 19:22