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 登录时遇到了问题。这是发生了什么:
- 用户在登录页面上选择 IWA 并点击提交按钮。这是通过 ajax 请求发送到常规登录操作的。
- 站点收到请求,看到 "use IWA" 选项并重定向到相关操作。已发送 302 响应。
- 浏览器自动处理 302 响应并调用重定向目标。
- 过滤器发现请求指向 IWA 登录操作并且 User.Identity.IsAuthenticated == false。发送 401 响应。
- 浏览器自动处理401响应。如果用户尚未在浏览器中使用 IWA 进行身份验证,他们会收到一个弹出窗口来执行此操作(默认浏览器行为)。收到凭据后,浏览器会使用用户凭据执行相同的请求。
- 站点收到经过身份验证的请求并模拟用户以对 Active Directory 执行检查。如果用户通过身份验证,我们使用上面的代码完成登录。
- 用户被转发到站点的主页。
- 站点收到加载主页的请求。 这就是有时出错的地方。
此时的 User.Identity
是类型 WindowsIdentity
,AuthenticationType
设置为 Negotiate
,并且 NOT 正如我所期望的那样,ClaimsIdentity
在上面的 SignIn
方法中创建。
该站点通过在视图中调用 @AntiForgery.GetHtml()
为用户准备主页。这样做是为了使用登录用户的详细信息创建一个新的防伪令牌。令牌是使用 WindowsIdentity
创建的
- 随着主页加载,向服务器发出的 ajax 请求随
ClaimsIdentity
一起到达!因此,第一个 POST
请求到达不可避免地会导致 AntiForgeryException
,其中它发送的防伪令牌是 "for a different user".
刷新页面会导致主页加载 ClaimsIdentity
并允许 POST
请求运行。
其次,相关问题:在刷新后的任何时候,一旦一切正常,POST
请求可能会到达 WindowsIdentity
而不是 ClaimsIdentity
,再次抛出 AntiForgeryException
.
- 它不是任何特定的post请求,
- 它是而不是在任何特定时间之后(可能是first/second请求,可能是第一百次),
- 这 不一定是 特定 post 请求在该会话期间第一次被调用。
我觉得我要么遗漏了有关 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
请求得到 ClaimsIdentity
但 POST
请求得到 WindowsIdentity
。这非常令人沮丧,我决定调试并将断点设置为 DefaultHttpContext.set_User
。 IISMiddleware
正在设置 User
属性,然后我注意到它有一个 AutomaticAuthentication
,默认值为 true
,它设置了 User
属性。我将其更改为 false,因此 HttpContext.User
一直变为 ClaimsPrincipal
,欢呼。
现在我的问题变成了如何使用 Windows 身份验证。幸运的是,即使我将 AutomaticAuthentication
设置为 false
,IISMiddleware
也会用 WindowsPrincipal
更新 HttpContext.Features
,所以 var windowsUser = HttpContext.Features.Get<WindowsPrincipal>();
returns Windows 我的 SSO 页面上的用户。
一切正常,顺利,没有波动,什么都没有。表单基础和 Windows 身份验证一起工作。
services.Configure<IISOptions>(opts =>
{
opts.AutomaticAuthentication = false;
});
我有一个 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 登录时遇到了问题。这是发生了什么:
- 用户在登录页面上选择 IWA 并点击提交按钮。这是通过 ajax 请求发送到常规登录操作的。
- 站点收到请求,看到 "use IWA" 选项并重定向到相关操作。已发送 302 响应。
- 浏览器自动处理 302 响应并调用重定向目标。
- 过滤器发现请求指向 IWA 登录操作并且 User.Identity.IsAuthenticated == false。发送 401 响应。
- 浏览器自动处理401响应。如果用户尚未在浏览器中使用 IWA 进行身份验证,他们会收到一个弹出窗口来执行此操作(默认浏览器行为)。收到凭据后,浏览器会使用用户凭据执行相同的请求。
- 站点收到经过身份验证的请求并模拟用户以对 Active Directory 执行检查。如果用户通过身份验证,我们使用上面的代码完成登录。
- 用户被转发到站点的主页。
- 站点收到加载主页的请求。 这就是有时出错的地方。
此时的User.Identity
是类型WindowsIdentity
,AuthenticationType
设置为Negotiate
,并且 NOT 正如我所期望的那样,ClaimsIdentity
在上面的SignIn
方法中创建。
该站点通过在视图中调用@AntiForgery.GetHtml()
为用户准备主页。这样做是为了使用登录用户的详细信息创建一个新的防伪令牌。令牌是使用WindowsIdentity
创建的
- 随着主页加载,向服务器发出的 ajax 请求随
ClaimsIdentity
一起到达!因此,第一个POST
请求到达不可避免地会导致AntiForgeryException
,其中它发送的防伪令牌是 "for a different user".
刷新页面会导致主页加载 ClaimsIdentity
并允许 POST
请求运行。
其次,相关问题:在刷新后的任何时候,一旦一切正常,POST
请求可能会到达 WindowsIdentity
而不是 ClaimsIdentity
,再次抛出 AntiForgeryException
.
- 它不是任何特定的post请求,
- 它是而不是在任何特定时间之后(可能是first/second请求,可能是第一百次),
- 这 不一定是 特定 post 请求在该会话期间第一次被调用。
我觉得我要么遗漏了有关 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 astatic Func<IEnumerable<ClaimsIdentity>, ClaimsIdentity>
calledPrimaryIdentitySelector
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
请求得到 ClaimsIdentity
但 POST
请求得到 WindowsIdentity
。这非常令人沮丧,我决定调试并将断点设置为 DefaultHttpContext.set_User
。 IISMiddleware
正在设置 User
属性,然后我注意到它有一个 AutomaticAuthentication
,默认值为 true
,它设置了 User
属性。我将其更改为 false,因此 HttpContext.User
一直变为 ClaimsPrincipal
,欢呼。
现在我的问题变成了如何使用 Windows 身份验证。幸运的是,即使我将 AutomaticAuthentication
设置为 false
,IISMiddleware
也会用 WindowsPrincipal
更新 HttpContext.Features
,所以 var windowsUser = HttpContext.Features.Get<WindowsPrincipal>();
returns Windows 我的 SSO 页面上的用户。
一切正常,顺利,没有波动,什么都没有。表单基础和 Windows 身份验证一起工作。
services.Configure<IISOptions>(opts =>
{
opts.AutomaticAuthentication = false;
});