使用与IdentityServer4集成的AspNetCore 2.0 Identity的部分登录工作流程

Mar*_*ark 8 asp.net-identity identityserver4

通过IdentityServer3和AspNet Identity,我们可以利用PostAuthenticateLocalAsync并返回AuthenticateResult,这允许我们支持不同的登录工作流程,例如双因素,EULA接受,以及强制用户更改其本地密码.因此,在完成这些工作流程之前,不会发布"真正的"经过身份验证的cookie.我认为这个过程被称为部分登录过程.

继续使用IdentityServer4和AspNet Core 2.0标识我们现在希望AspNet Identity组件能够完全管理登录工作流程,从而消除IS4对支持部分登录过程的责任.

我查看了AspNet Identity的SignInManager的代码,看看2fa登录是如何工作的,因为我希望它以部分登录方式运行,即在2fa握手完成之前不会发出正确的cookie.它似乎通过使用IdentityConstants.TwoFactorUserIdScheme临时存储2fa信息来实现.

本地和外部登录进程都调用SignInOrTwoFactorAsync,因此我认为我可以在自定义SignInManager中覆盖此方法并执行类似于IdentityServer3的原始部分登录的行为(然后在部分流完成后找出继续登录的方法).换句话说,我会阻止SignInAsync生成一个真正的cookie,直到完成部分流程.

有没有人尝试使用当前的AspNetCore 2.0身份?如果是这样,是否有任何开源示例?

如果这是实现部分登录流程的正确或首选方式,我希望我所讨论的方法的名称可以更少地与2fa耦合.

Mar*_*ark 2

我自己确实解决了这个问题,所以为了帮助其他人,我将发布这个答案。我将使用强制用户在登录后更改其本地密码的示例。

创建一个派生自 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);
}
Run Code Online (Sandbox Code Playgroud)

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);
}
Run Code Online (Sandbox Code Playgroud)

请注意,我们正在登录名为“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;
  });
Run Code Online (Sandbox Code Playgroud)

到目前为止,当我们希望更改密码时,我们能够将用户重定向到自定义页面,并且登录将创建一个代表我们部分登录的 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);
}
Run Code Online (Sandbox Code Playgroud)

...还有一个自定义类型只是为了保存用户 ID - 有点冗长,但这只是复制现有的身份代码。

internal class PasswordChangeAuthenticationInfo
{
   public string UserId { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

LoginWithChangePassword 调用GetPasswordChangeAuthenticationUserAsync以获取使用自定义方案进行身份验证的用户。如果返回值为 null,则身份验证失败,用户应重定向到其他地方。如果我们有经过身份验证的用户(检查User.Identity.AuthenticationType以确保它来自我们的方案),请接受用户的更改。成功更改数据后,您可以调用SignOutAysncSignInManager,例如:

if (HttpContext.User.Identity.AuthenticationType == PasswordChangeScheme)
{
   await _signInManager.SignOutAsync();
}
Run Code Online (Sandbox Code Playgroud)

这会迫使用户返回登录页面以尝试新密码。

希望能帮助到你。