通过IdentityServer3和AspNet Identity,我们能够利用PostAuthenticateLocalAsync并返回AuthenticateResult,这使我们能够支持不同的登录工作流,如双重因素、EULA接受和强制用户更改其本地密码。因此,在这些工作流完成之前,不会发布"真正的"经过身份验证的cookie。我认为这个过程被称为部分登录过程。
转到IdentityServer4和AspNet Core 2.0 Identity,我们现在希望AspNet Identity组件能够完全管理登录工作流,从而免除IS4支持部分登录过程的责任。
我查看了AspNet Identity的SignInManager的代码,以了解2fa登录是如何工作的,正如我所期望的那样,它以部分登录的方式运行,即在2fa握手完成之前不发出正确的cookie。它似乎是通过使用IdentityConstants.TwoFactorUserIdScheme临时存储2fa信息来实现的。
本地和外部登录进程都调用SignInOrTwoFactorAsync,所以我认为我可以在自定义SignInManager中重写此方法,并执行类似于IdentityServer3原始部分登录的行为(然后在部分流完成后找到继续登录的方法)。换句话说,在完成部分流之前,我会阻止SignInAsync生成真正的cookie。
是否有人使用当前的AspNetCore 2.0标识尝试过此操作?如果是,有没有开源的例子?
如果这是实现部分登录流的正确或首选方式,我希望我所说的重写方法的名称与2fa的耦合度较低。
我自己解决了这个问题,所以为了帮助别人,我会发布这个答案。我将使用强制用户在登录后更改本地密码的示例。
创建一个从ApplicationSigninManager派生的类,并重写SignInOrTwoFactorAsync。这是任何约束逻辑都应该运行的地方,以检查是否应该出现本地或外部登录以外的登录流——我们的部分登录。
因此,对于密码更改检查,我可能会做以下操作:
protected override async Task<SignInResult> SignInOrTwoFactorAsync(ApplicationUser user, bool isPersistent, string loginProvider = null, bool bypassTwoFactor = false)
{
var userId = await UserManager.GetUserIdAsync(user);
var hasPassword = await UserManager.HasPasswordAsync(user);
if (hasPassword && user.PasswordChangeRequired)
{
// Store the userId for use after password change
await Context.SignInAsync(PasswordChangeScheme, StorePasswordChangeInfo(userId));
return ApplicationSignInResult.RequiresPasswordChange;
}
return await base.SignInOrTwoFactorAsync(user, isPersistent, loginProvider, bypassTwoFactor);
}
ApplicationSignInResult
只是一个自定义派生的SignInResult,它允许跟踪扩展签名状态;需要更改密码,需要接受条款等。当用户发布登录信息时,登录控制器操作/剃刀页面将检查此状态,以确定是否需要重定向到自定义页面。在我们的示例案例中,LoginWithChangePassword将是我们的自定义页面。
StorePasswordChangeInfo
方法的工作原理与SigninManager的内部StoreTwoFactorInfo方法类似,因为它只获取所需方案的标识,添加支持部分流所需的任何临时信息(在我们的示例中是用户Id),并从扩展标识返回新的ClaimsPrincipal。
internal ClaimsPrincipal StorePasswordChangeInfo(string userId)
{
var identity = new ClaimsIdentity(PasswordChangeScheme);
identity.AddClaim(new Claim(ClaimTypes.Name, userId));
return new ClaimsPrincipal(identity);
}
请注意,我们正在登录一个名为PasswordChangeScheme的自定义身份验证方案。我们还需要一个特定于此方案的cookie,以便我们可以维护特定于部分流的状态。这与应用程序服务配置期间的任何其他cookie身份验证一样设置:
// Authentication schemes and cookies
services.AddAuthentication()
// Support partial sign in workflow to force user to change their password
.AddCookie(PasswordChangeScheme, options =>
{
// We don't want these cookies to last for a long time.
options.ExpireTimeSpan = TimeSpan.FromMinutes(5);
options.Cookie.Name = PasswordChangeScheme;
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
到目前为止,当我们希望更改密码时,我们可以将用户重定向到一个自定义页面,签名将创建一个代表我们部分登录的cookie。
我们的自定义更改密码页面现在需要处理用户表单数据,并将更改持久化到用户配置文件中。
设置页面(假设此处为Razor页面,但同样适用于控制器)[AllowAnonymous]
属性以覆盖可能设置的任何其他身份验证策略。大多数";帐户";页面默认情况下是匿名的。
在对表单数据执行任何操作之前,页面的Get和Post处理程序必须使用我们的自定义方案对用户进行身份验证。以类似于2fa的方式,我们在自定义SignInManager:中提供了更多功能
private async Task<PasswordChangeAuthenticationInfo> RetrievePasswordChangeInfoAsync()
{
var result = await Context.AuthenticateAsync(PasswordChangeScheme);
if (result?.Principal != null)
{
return new PasswordChangeAuthenticationInfo
{
UserId = result.Principal.FindFirstValue(ClaimTypes.Name),
};
}
return null;
}
public virtual async Task<ApplicationUser> GetPasswordChangeAuthenticationUserAsync()
{
var info = await RetrievePasswordChangeInfoAsync();
if (info == null)
{
return null;
}
return await UserManager.FindByIdAsync(info.UserId);
}
和一个只保存用户id的自定义类型——有点冗长,但这只是复制现有的身份代码。
internal class PasswordChangeAuthenticationInfo
{
public string UserId { get; set; }
}
LoginWithChangePassword调用GetPasswordChangeAuthenticationUserAsync
以获得使用自定义方案进行身份验证的用户。如果返回值为null,则身份验证失败,用户应该重定向到其他地方。如果我们有一个经过身份验证的用户(检查User.Identity.AuthenticationType
以确保它来自我们的方案),则接受用户的更改。成功更改数据后,您可以在SignInManager上调用SignOutAysnc
,例如:
if (HttpContext.User.Identity.AuthenticationType == PasswordChangeScheme)
{
await _signInManager.SignOutAsync();
}
这将强制用户返回登录页面以尝试他们的新密码。
希望能有所帮助。