From 69ac9abcc5bdd831026bb31983b9e4f2979915e5 Mon Sep 17 00:00:00 2001 From: Hao Kung Date: Mon, 12 Jan 2015 12:47:08 -0800 Subject: [PATCH] Rehash passwords when needed Fixes https://github.com/aspnet/Identity/issues/17 --- src/Microsoft.AspNet.Identity/UserManager.cs | 40 +++++++++++-------- .../UserManagerTest.cs | 28 +++++++++++++ 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/src/Microsoft.AspNet.Identity/UserManager.cs b/src/Microsoft.AspNet.Identity/UserManager.cs index 1f8f4b663a..e7897a7e1a 100644 --- a/src/Microsoft.AspNet.Identity/UserManager.cs +++ b/src/Microsoft.AspNet.Identity/UserManager.cs @@ -463,7 +463,7 @@ namespace Microsoft.AspNet.Identity { throw new ArgumentNullException("password"); } - var result = await UpdatePasswordInternal(passwordStore, user, password, cancellationToken); + var result = await UpdatePasswordHash(passwordStore, user, password, cancellationToken); if (!result.Succeeded) { return result; @@ -565,7 +565,12 @@ namespace Microsoft.AspNet.Identity { return false; } - return await VerifyPasswordAsync(passwordStore, user, password, cancellationToken); + var result = await VerifyPasswordAsync(passwordStore, user, password, cancellationToken); + if (result == PasswordVerificationResult.SuccessRehashNeeded) { + await UpdatePasswordHash(passwordStore, user, password, cancellationToken, validatePassword: false); + await UpdateAsync(user, cancellationToken); + } + return result != PasswordVerificationResult.Failed; } /// @@ -607,7 +612,7 @@ namespace Microsoft.AspNet.Identity { return IdentityResult.Failed(ErrorDescriber.UserAlreadyHasPassword()); } - var result = await UpdatePasswordInternal(passwordStore, user, password, cancellationToken); + var result = await UpdatePasswordHash(passwordStore, user, password, cancellationToken); if (!result.Succeeded) { return result; @@ -632,9 +637,9 @@ namespace Microsoft.AspNet.Identity { throw new ArgumentNullException("user"); } - if (await VerifyPasswordAsync(passwordStore, user, currentPassword, cancellationToken)) + if (await VerifyPasswordAsync(passwordStore, user, currentPassword, cancellationToken) != PasswordVerificationResult.Failed) { - var result = await UpdatePasswordInternal(passwordStore, user, newPassword, cancellationToken); + var result = await UpdatePasswordHash(passwordStore, user, newPassword, cancellationToken); if (!result.Succeeded) { return result; @@ -659,21 +664,24 @@ namespace Microsoft.AspNet.Identity { throw new ArgumentNullException("user"); } - await passwordStore.SetPasswordHashAsync(user, null, cancellationToken); - await UpdateSecurityStampInternal(user, cancellationToken); + await UpdatePasswordHash(passwordStore, user, null, cancellationToken, validatePassword: false); return await UpdateAsync(user, cancellationToken); } - internal async Task UpdatePasswordInternal(IUserPasswordStore passwordStore, - TUser user, string newPassword, CancellationToken cancellationToken) + internal async Task UpdatePasswordHash(IUserPasswordStore passwordStore, + TUser user, string newPassword, CancellationToken cancellationToken, bool validatePassword = true) { - var validate = await ValidatePasswordInternal(user, newPassword, cancellationToken); - if (!validate.Succeeded) + if (validatePassword) { - return validate; + var validate = await ValidatePasswordInternal(user, newPassword, cancellationToken); + if (!validate.Succeeded) + { + return validate; + } } + var hash = newPassword != null ? PasswordHasher.HashPassword(user, newPassword) : null; await - passwordStore.SetPasswordHashAsync(user, PasswordHasher.HashPassword(user, newPassword), cancellationToken); + passwordStore.SetPasswordHashAsync(user, hash, cancellationToken); await UpdateSecurityStampInternal(user, cancellationToken); return IdentityResult.Success; } @@ -686,11 +694,11 @@ namespace Microsoft.AspNet.Identity /// /// /// - protected virtual async Task VerifyPasswordAsync(IUserPasswordStore store, TUser user, + protected virtual async Task VerifyPasswordAsync(IUserPasswordStore store, TUser user, string password, CancellationToken cancellationToken = default(CancellationToken)) { var hash = await store.GetPasswordHashAsync(user, cancellationToken); - return PasswordHasher.VerifyHashedPassword(user, hash, password) != PasswordVerificationResult.Failed; + return PasswordHasher.VerifyHashedPassword(user, hash, password); } // IUserSecurityStampStore methods @@ -776,7 +784,7 @@ namespace Microsoft.AspNet.Identity return IdentityResult.Failed(ErrorDescriber.InvalidToken()); } var passwordStore = GetPasswordStore(); - var result = await UpdatePasswordInternal(passwordStore, user, newPassword, cancellationToken); + var result = await UpdatePasswordHash(passwordStore, user, newPassword, cancellationToken); if (!result.Succeeded) { return result; diff --git a/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs b/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs index b340caa63f..e4cf94ecd5 100644 --- a/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs @@ -415,6 +415,34 @@ namespace Microsoft.AspNet.Identity.Test store.VerifyAll(); } + [Fact] + public async Task CheckPasswordWillRehashPasswordWhenNeeded() + { + // Setup + var store = new Mock>(); + var hasher = new Mock>(); + var user = new TestUser { UserName = "Foo" }; + var pwd = "password"; + var hashed = "hashed"; + var rehashed = "rehashed"; + + store.Setup(s => s.GetPasswordHashAsync(user, CancellationToken.None)) + .ReturnsAsync(hashed) + .Verifiable(); + hasher.Setup(s => s.VerifyHashedPassword(user, hashed, pwd)).Returns(PasswordVerificationResult.SuccessRehashNeeded).Verifiable(); + hasher.Setup(s => s.HashPassword(user, pwd)).Returns(rehashed).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + userManager.PasswordHasher = hasher.Object; + + // Act + var result = await userManager.CheckPasswordAsync(user, pwd); + + // Assert + Assert.True(result); + store.VerifyAll(); + hasher.VerifyAll(); + } + [Fact] public async Task RemoveClaimsCallsStore() {