ASP.NET Identity 重置密码

Posted

技术标签:

【中文标题】ASP.NET Identity 重置密码【英文标题】:ASP.NET Identity reset password 【发布时间】:2013-10-31 16:36:14 【问题描述】:

如何在新的 ASP.NET 身份系统中获取用户的密码?或者如何在不知道当前密码的情况下重置(用户忘记密码)?

【问题讨论】:

【参考方案1】:

在用于 Web API 的 Asp.Net Core Identity 中重置密码的最佳方法。

注意* :Error() 和 Result() 是为内部使用而创建的。你可以退货。

        [HttpPost]
        [Route("reset-password")]
        public async Task<IActionResult> ResetPassword(ResetPasswordModel model)
        
            if (!ModelState.IsValid)
                return BadRequest(ModelState);
            try
            
                if (model is null)
                    return Error("No data found!");


                var user = await _userManager.FindByIdAsync(AppCommon.ToString(GetUserId()));
                if (user == null)
                    return Error("No user found!");

                Microsoft.AspNetCore.Identity.SignInResult checkOldPassword =
                    await _signInManager.PasswordSignInAsync(user.UserName, model.OldPassword, false, false);

                if (!checkOldPassword.Succeeded)
                    return Error("Old password does not matched.");

                string resetToken = await _userManager.GeneratePasswordResetTokenAsync(user);
                if (string.IsNullOrEmpty(resetToken))
                    return Error("Error while generating reset token.");

                var result = await _userManager.ResetPasswordAsync(user, resetToken, model.Password);

                if (result.Succeeded)
                    return Result();
                else
                    return Error();
            
            catch (Exception ex)
            
                return Error(ex);
            
        

【讨论】:

这对我也适用于 Fx v 4.5。另一个解决方案不起作用。从根本上说,这也简单得多。您甚至不需要获取用户,因为所有方法都将接受 id。我只需要在我的管理界面中进行一次临时的一次性重置,所以我不需要所有的错误检查。【参考方案2】:

我做了一些调查,对我有用的解决方案是本文中提出的一些解决方案的组合。

我基本上是在编译这个解决方案,并发布对我有用的东西。就我而言,我不想使用 .net 核心中的任何令牌。

public async Task ResetPassword(string userId, string password)

    var user = await _userManager.FindByIdAsync(userId);
    var hashPassword= _userManager.PasswordHasher.HashPassword(user, password);
    user.PasswordHash = passwordHash;
    await _userManager.UpdateAsync(user);


【讨论】:

“什么对我有用”对于安全相关的东西来说还不够好。我想尽可能多地使用预制的 .NET Core。【参考方案3】:

我认为 Microsoft ASP.NET Identity 指南是一个好的开始。

https://docs.microsoft.com/en-us/aspnet/identity/overview/features-api/account-confirmation-and-password-recovery-with-aspnet-identity

注意:

如果您不使用 AccountController 并且不想重置密码,请使用 Request.GetOwinContext().GetUserManager&lt;ApplicationUserManager&gt;();。如果您没有相同的 OwinContext,则需要创建一个新的 DataProtectorTokenProvider,就像 OwinContext 使用的一样。默认情况下查看App_Start -&gt; IdentityConfig.cs。应该类似于new DataProtectorTokenProvider&lt;ApplicationUser&gt;(dataProtectionProvider.Create("ASP.NET Identity"));

可以这样创建:

没有欧文:

[HttpGet]
[AllowAnonymous]
[Route("testReset")]
public IHttpActionResult TestReset()

    var db = new ApplicationDbContext();
    var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(db));
    var provider = new DpapiDataProtectionProvider("SampleAppName");
    manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(
        provider.Create("SampleTokenName"));

    var email = "test@test.com";

    var user = new ApplicationUser()  UserName = email, Email = email ;

    var identityUser = manager.FindByEmail(email);

    if (identityUser == null)
    
        manager.Create(user);
        identityUser = manager.FindByEmail(email);
    

    var token = manager.GeneratePasswordResetToken(identityUser.Id);
    return Ok(HttpUtility.UrlEncode(token));


[HttpGet]
[AllowAnonymous]
[Route("testReset")]
public IHttpActionResult TestReset(string token)

    var db = new ApplicationDbContext();
    var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(db));
    var provider = new DpapiDataProtectionProvider("SampleAppName");
    manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(
        provider.Create("SampleTokenName"));
    var email = "test@test.com";
    var identityUser = manager.FindByEmail(email);
    var valid = Task.Run(() => manager.UserTokenProvider.ValidateAsync("ResetPassword", token, manager, identityUser)).Result;
    var result = manager.ResetPassword(identityUser.Id, token, "TestingTest1!");
    return Ok(result);

与欧文:

[HttpGet]
[AllowAnonymous]
[Route("testResetWithOwin")]
public IHttpActionResult TestResetWithOwin()

    var manager = Request.GetOwinContext().GetUserManager<ApplicationUserManager>();

    var email = "test@test.com";

    var user = new ApplicationUser()  UserName = email, Email = email ;

    var identityUser = manager.FindByEmail(email);

    if (identityUser == null)
    
        manager.Create(user);
        identityUser = manager.FindByEmail(email);
    

    var token = manager.GeneratePasswordResetToken(identityUser.Id);
    return Ok(HttpUtility.UrlEncode(token));


[HttpGet]
[AllowAnonymous]
[Route("testResetWithOwin")]
public IHttpActionResult TestResetWithOwin(string token)

    var manager = Request.GetOwinContext().GetUserManager<ApplicationUserManager>();

    var email = "test@test.com";
    var identityUser = manager.FindByEmail(email);
    var valid = Task.Run(() => manager.UserTokenProvider.ValidateAsync("ResetPassword", token, manager, identityUser)).Result;
    var result = manager.ResetPassword(identityUser.Id, token, "TestingTest1!");
    return Ok(result);

需要使用相同的名称创建 DpapiDataProtectionProviderDataProtectorTokenProvider 才能进行密码重置。使用 Owin 创建密码重置令牌,然后使用另一个名称创建新的 DpapiDataProtectionProvider 将不起作用。

我用于 ASP.NET 身份的代码:

Web.Config:

<add key="AllowedHosts" value="example.com,example2.com" />

AccountController.cs:

[Route("RequestResetPasswordToken/email/")]
[HttpGet]
[AllowAnonymous]
public async Task<IHttpActionResult> GetResetPasswordToken([FromUri]string email)

    if (!ModelState.IsValid)
        return BadRequest(ModelState);

    var user = await UserManager.FindByEmailAsync(email);
    if (user == null)
    
        Logger.Warn("Password reset token requested for non existing email");
        // Don't reveal that the user does not exist
        return NoContent();
    

    //Prevent Host Header Attack -> Password Reset Poisoning. 
    //If the IIS has a binding to accept connections on 80/443 the host parameter can be changed.
    //See https://security.stackexchange.com/a/170759/67046
    if (!ConfigurationManager.AppSettings["AllowedHosts"].Split(',').Contains(Request.RequestUri.Host)) 
            Logger.Warn($"Non allowed host detected for password reset Request.RequestUri.Scheme://Request.Headers.Host");
            return BadRequest();
    

    Logger.Info("Creating password reset token for user id 0", user.Id);

    var host = $"Request.RequestUri.Scheme://Request.Headers.Host";
    var token = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
    var callbackUrl = $"host/resetPassword/HttpContext.Current.Server.UrlEncode(user.Email)/HttpContext.Current.Server.UrlEncode(token)";

    var subject = "Client - Password reset.";
    var body = "<html><body>" +
               "<h2>Password reset</h2>" +
               $"<p>Hi user.FullName, <a href=\"callbackUrl\"> please click this link to reset your password </a></p>" +
               "</body></html>";

    var message = new IdentityMessage
    
        Body = body,
        Destination = user.Email,
        Subject = subject
    ;

    await UserManager.EmailService.SendAsync(message);

    return NoContent();


[HttpPost]
[Route("ResetPassword/")]
[AllowAnonymous]
public async Task<IHttpActionResult> ResetPasswordAsync(ResetPasswordRequestModel model)

    if (!ModelState.IsValid)
        return NoContent();

    var user = await UserManager.FindByEmailAsync(model.Email);
    if (user == null)
    
        Logger.Warn("Reset password request for non existing email");
        return NoContent();
                

    if (!await UserManager.UserTokenProvider.ValidateAsync("ResetPassword", model.Token, UserManager, user))
    
        Logger.Warn("Reset password requested with wrong token");
        return NoContent();
    

    var result = await UserManager.ResetPasswordAsync(user.Id, model.Token, model.NewPassword);

    if (result.Succeeded)
    
        Logger.Info("Creating password reset token for user id 0", user.Id);

        const string subject = "Client - Password reset success.";
        var body = "<html><body>" +
                   "<h1>Your password for Client was reset</h1>" +
                   $"<p>Hi user.FullName!</p>" +
                   "<p>Your password for Client was reset. Please inform us if you did not request this change.</p>" +
                   "</body></html>";

        var message = new IdentityMessage
        
            Body = body,
            Destination = user.Email,
            Subject = subject
        ;

        await UserManager.EmailService.SendAsync(message);
    

    return NoContent();


public class ResetPasswordRequestModel

    [Required]
    [Display(Name = "Token")]
    public string Token  get; set; 

    [Required]
    [Display(Name = "Email")]
    public string Email  get; set; 

    [Required]
    [StringLength(100, ErrorMessage = "The 0 must be at least 2 characters long.", MinimumLength = 10)]
    [DataType(DataType.Password)]
    [Display(Name = "New password")]
    public string NewPassword  get; set; 

    [DataType(DataType.Password)]
    [Display(Name = "Confirm new password")]
    [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
    public string ConfirmPassword  get; set; 

【讨论】:

【参考方案4】:

如果密码重置,建议通过将密码重置令牌发送到注册用户电子邮件并要求用户提供新密码来重置密码。如果已经使用默认配置设置在 Identity 框架上创建了一个易于使用的 .NET 库。您可以在 github 上的 blog link 和 source code 找到详细信息。

【讨论】:

【参考方案5】:

UserManager&lt;TUser, TKey&gt;中创建方法

public Task<IdentityResult> ChangePassword(int userId, string newPassword)

     var user = Users.FirstOrDefault(u => u.Id == userId);
     if (user == null)
          return new Task<IdentityResult>(() => IdentityResult.Failed());

     var store = Store as IUserPasswordStore<User, int>;
     return base.UpdatePassword(store, user, newPassword);

【讨论】:

【参考方案6】:

已弃用

这是最初的答案。它确实有效,但有一个问题。如果AddPassword 失败了怎么办?用户没有密码。

原答案:我们可以用三行代码:

UserManager<IdentityUser> userManager = 
    new UserManager<IdentityUser>(new UserStore<IdentityUser>());

userManager.RemovePassword(userId);

userManager.AddPassword(userId, newPassword);

另请参阅:http://msdn.microsoft.com/en-us/library/dn457095(v=vs.111).aspx

现在推荐

使用EdwardBrey proposed 和DanielWright later elaborated 的答案可能会更好。

【讨论】:

感谢上帝,我想我必须创建一个新的用户商店,直到我看到这个! 有没有办法直接在 SQL 中执行此操作?我很乐意在需要时向我的 DBA 提供一个存储过程来调用,而不是一个可执行文件。 @MarkRichman 这是一个新问题。不过,您可以做的一件事是检查生成的在 SQL Server 上运行的 T-SQL。 开启此功能请注意,每当 AddPassword 失败(即密码复杂性不足)时,用户将失去密码。 Daniel Wright 提出的最简洁的方法是不绕过任何业务规则(因为当您直接访问密码哈希时,没有密码复杂性验证)。【参考方案7】:

或者如何在不知道当前密码的情况下重置(用户忘记密码)?

如果您想使用 UserManager 更改密码但不想提供用户的当前密码,您可以生成密码重置令牌,然后立即使用它。

string resetToken = await UserManager.GeneratePasswordResetTokenAsync(model.Id);
IdentityResult passwordChangeResult = await UserManager.ResetPasswordAsync(model.Id, resetToken, model.NewPassword);

【讨论】:

这是迄今为止设置新密码的最佳和最干净的方法。接受答案的问题在于它通过直接访问密码哈希绕过密码复杂性验证。 仅供参考,您可能会收到错误消息“未注册 IUserTokenProvider。”如果您使用上述逻辑。看到这个***.com/questions/22629936/…。 我想这仅适用于版本 2 中的 Microsoft.AspNet.Identity。在版本 1 中找不到 GeneratePasswordResetTokenAsync 方法。 感谢您的回答。它对我来说就像一种魅力。 如果您得到 Invalid Token,请确保您的用户的 SecurityStamp 不为空。对于从其他数据库迁移过来的用户,或者不是通过UserManager.CreateAsync()方法创建的用户,可能会出现这种情况。【参考方案8】:

在您的UserManager 上,首先致电GeneratePasswordResetTokenAsync。一旦用户验证了他的身份(例如通过在电子邮件中接收令牌),将令牌传递给ResetPasswordAsync。

【讨论】:

试图弄清楚为什么 ResetPasswordAsync 需要用户 ID,以及当用户使用令牌出现时从用户那里获取它的合理方法。 GeneratePasswordReset 使用超过 150 个字符的令牌......似乎这足以以加密方式存储用户 ID,所以我不必自己实现。 :( 我假设它正在询问用户 ID,因此它可以针对该用户 ID 将重置令牌输入身份数据库。如果它不这样做,框架将如何知道令牌是否有效。您应该能够使用 User.Identity.GetUserId() 或类似方法提取用户 ID。 要求用户 id 对 API 来说是一个愚蠢的选择,当调用 ResetPassword(async) 时,令牌已经在数据库中,只需根据输入验证它就足够了。跨度> @Filip,ResetPasswordAsync 获取用户 ID 的优点是身份提供者只需要索引用户 ID,而不需要标记。如果有很多用户,它可以更好地扩展。 @Edward Brey,你如何获取重置调用的用户 ID?【参考方案9】:

在当前版本中

假设您已经处理了重置忘记密码请求的验证,请使用以下代码作为示例代码步骤。

ApplicationDbContext =new ApplicationDbContext()
String userId = "<YourLogicAssignsRequestedUserId>";
String newPassword = "<PasswordAsTypedByUser>";
ApplicationUser cUser = UserManager.FindById(userId);
String hashedNewPassword = UserManager.PasswordHasher.HashPassword(newPassword);
UserStore<ApplicationUser> store = new UserStore<ApplicationUser>();            
store.SetPasswordHashAsync(cUser, hashedNewPassword);

在 AspNet Nightly Build 中

框架已更新,可与 Token 一起处理 ForgetPassword 等请求。一旦发布,预计会有简单的代码指导。

更新:

本次更新只是为了提供更清晰的步骤。

ApplicationDbContext context = new ApplicationDbContext();
UserStore<ApplicationUser> store = new UserStore<ApplicationUser>(context);
UserManager<ApplicationUser> UserManager = new UserManager<ApplicationUser>(store);
String userId = User.Identity.GetUserId();//"<YourLogicAssignsRequestedUserId>";
String newPassword = "test@123"; //"<PasswordAsTypedByUser>";
String hashedNewPassword = UserManager.PasswordHasher.HashPassword(newPassword);                    
ApplicationUser cUser = await store.FindByIdAsync(userId);
await store.SetPasswordHashAsync(cUser, hashedNewPassword);
await store.UpdateAsync(cUser);

【讨论】:

你知道1.1版本什么时候发布吗? 它仍处于 alpha 阶段,1.0 刚刚发布。所以假设很多个月。 myget.org/gallery/aspnetwebstacknightly 奇怪的是 store.SetPasswordHashAsync(cUser, hashedNewPassword) 方法调用对我不起作用,而是我不得不手动设置 cUser.PasswordHash = hashedNewPassword 然后调用 UserManager.UpdateAsync(user); 只有当用户检索上下文和存储上下文不同时,代码才可能工作。该代码只是一个示例步骤,并不准确。将很快更新答案以避免其他人出现此问题。 框架 1 不提供。但是 Framework 2-alpha 确实有一些功能可以提供简单的过程来处理密码重置请求。 aspnetidentity.codeplex.com【参考方案10】:
string message = null;
//reset the password
var result = await IdentityManager.Passwords.ResetPasswordAsync(model.Token, model.Password);
if (result.Success)

    message = "The password has been reset.";
    return RedirectToAction("PasswordResetCompleted", new  message = message );

else

    AddErrors(result);

这段代码是从 AspNetIdentitySample 项目available on github中取出来的

【讨论】:

以上是关于ASP.NET Identity 重置密码的主要内容,如果未能解决你的问题,请参考以下文章

ASP.Net Identity 2 使用短信重置密码

Asp.Net Identity 2 无法重置密码明文?

ASP.NET CORE Identity DataProtectionTokenProviderOptions 密码重置令牌无效

快速掌握 ASP.NET 身份认证框架 Identity - 通过邮件重置密码

ASP.NET Core Identity - 使令牌(电子邮件确认、密码重置等)在不同服务器上有效?

无需输入电子邮件即可重置密码(ASP.NET 身份)