From b355f59a013f263e7d41dbeca518ea96866640f0 Mon Sep 17 00:00:00 2001 From: Hao Kung Date: Thu, 6 Jul 2017 13:14:47 -0700 Subject: [PATCH] TFA tweaks for templates (#1299) --- .../UserManagerSpecificationTests.cs | 2 +- .../IUserTwoFactorRecoveryCodeStore.cs | 8 ++++++ .../UserManager.cs | 25 +++++++++++++---- .../UserStoreBase.cs | 27 ++++++++++++++++--- .../InMemoryUserStore.cs | 10 +++++++ 5 files changed, 62 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.AspNetCore.Identity.Specification.Tests/UserManagerSpecificationTests.cs b/src/Microsoft.AspNetCore.Identity.Specification.Tests/UserManagerSpecificationTests.cs index 0cc5f8e3b0..df5dae884c 100644 --- a/src/Microsoft.AspNetCore.Identity.Specification.Tests/UserManagerSpecificationTests.cs +++ b/src/Microsoft.AspNetCore.Identity.Specification.Tests/UserManagerSpecificationTests.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Linq.Expressions; using System.Security.Claims; using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -1890,6 +1889,7 @@ namespace Microsoft.AspNetCore.Identity.Test { IdentityResultAssert.IsSuccess(await manager.RedeemTwoFactorRecoveryCodeAsync(user, code)); IdentityResultAssert.IsFailure(await manager.RedeemTwoFactorRecoveryCodeAsync(user, code)); + Assert.Equal(--numCodes, await manager.CountRecoveryCodesAsync(user)); } // One last time to be sure foreach (var code in newCodes) diff --git a/src/Microsoft.Extensions.Identity.Core/IUserTwoFactorRecoveryCodeStore.cs b/src/Microsoft.Extensions.Identity.Core/IUserTwoFactorRecoveryCodeStore.cs index fdc1544b13..9de8e2e5ce 100644 --- a/src/Microsoft.Extensions.Identity.Core/IUserTwoFactorRecoveryCodeStore.cs +++ b/src/Microsoft.Extensions.Identity.Core/IUserTwoFactorRecoveryCodeStore.cs @@ -31,5 +31,13 @@ namespace Microsoft.AspNetCore.Identity /// The used to propagate notifications that the operation should be canceled. /// True if the recovery code was found for the user. Task RedeemCodeAsync(TUser user, string code, CancellationToken cancellationToken); + + /// + /// Returns how many recovery code are still valid for a user. + /// + /// The user who owns the recovery code. + /// The used to propagate notifications that the operation should be canceled. + /// The number of valid recovery codes for the user.. + Task CountCodesAsync(TUser user, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Microsoft.Extensions.Identity.Core/UserManager.cs b/src/Microsoft.Extensions.Identity.Core/UserManager.cs index 0a9f212374..8ced663058 100644 --- a/src/Microsoft.Extensions.Identity.Core/UserManager.cs +++ b/src/Microsoft.Extensions.Identity.Core/UserManager.cs @@ -2167,7 +2167,7 @@ namespace Microsoft.AspNetCore.Identity /// /// The user to generate recovery codes for. /// The number of codes to generate. - /// The new recovery codes for the user. + /// The new recovery codes for the user. Note: there may be less than number returned, as duplicates will be removed. public virtual async Task> GenerateNewTwoFactorRecoveryCodesAsync(TUser user, int number) { ThrowIfDisposed(); @@ -2183,7 +2183,7 @@ namespace Microsoft.AspNetCore.Identity newCodes.Add(CreateTwoFactorRecoveryCode()); } - await store.ReplaceCodesAsync(user, newCodes, CancellationToken); + await store.ReplaceCodesAsync(user, newCodes.Distinct(), CancellationToken); var update = await UpdateAsync(user); if (update.Succeeded) { @@ -2197,9 +2197,7 @@ namespace Microsoft.AspNetCore.Identity /// /// protected virtual string CreateTwoFactorRecoveryCode() - { - return Guid.NewGuid().ToString(); - } + => Guid.NewGuid().ToString().Substring(0, 8); /// /// Returns whether a recovery code is valid for a user. Note: recovery codes are only valid @@ -2225,6 +2223,23 @@ namespace Microsoft.AspNetCore.Identity return IdentityResult.Failed(ErrorDescriber.RecoveryCodeRedemptionFailed()); } + /// + /// Returns how many recovery code are still valid for a user. + /// + /// The user. + /// How many recovery code are still valid for a user. + public virtual Task CountRecoveryCodesAsync(TUser user) + { + ThrowIfDisposed(); + var store = GetRecoveryCodeStore(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + return store.CountCodesAsync(user, CancellationToken); + } + /// /// Releases the unmanaged resources used by the role manager and optionally releases the managed resources. /// diff --git a/src/Microsoft.Extensions.Identity.Stores/UserStoreBase.cs b/src/Microsoft.Extensions.Identity.Stores/UserStoreBase.cs index 749cbaf8e6..a8259092ca 100644 --- a/src/Microsoft.Extensions.Identity.Stores/UserStoreBase.cs +++ b/src/Microsoft.Extensions.Identity.Stores/UserStoreBase.cs @@ -992,9 +992,7 @@ namespace Microsoft.AspNetCore.Identity /// The used to propagate notifications that the operation should be canceled. /// The that represents the asynchronous operation. public virtual Task SetAuthenticatorKeyAsync(TUser user, string key, CancellationToken cancellationToken) - { - return SetTokenAsync(user, InternalLoginProvider, AuthenticatorKeyTokenName, key, cancellationToken); - } + => SetTokenAsync(user, InternalLoginProvider, AuthenticatorKeyTokenName, key, cancellationToken); /// /// Get the authenticator key for the specified . @@ -1003,8 +1001,29 @@ namespace Microsoft.AspNetCore.Identity /// The used to propagate notifications that the operation should be canceled. /// The that represents the asynchronous operation, containing the security stamp for the specified . public virtual Task GetAuthenticatorKeyAsync(TUser user, CancellationToken cancellationToken) + => GetTokenAsync(user, InternalLoginProvider, AuthenticatorKeyTokenName, cancellationToken); + + /// + /// Returns how many recovery code are still valid for a user. + /// + /// The user who owns the recovery code. + /// The used to propagate notifications that the operation should be canceled. + /// The number of valid recovery codes for the user.. + public virtual async Task CountCodesAsync(TUser user, CancellationToken cancellationToken) { - return GetTokenAsync(user, InternalLoginProvider, AuthenticatorKeyTokenName, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + var mergedCodes = await GetTokenAsync(user, InternalLoginProvider, RecoveryCodeTokenName, cancellationToken) ?? ""; + if (mergedCodes.Length > 0) + { + return mergedCodes.Split(';').Length; + } + return 0; } /// diff --git a/test/Microsoft.AspNetCore.Identity.InMemory.Test/InMemoryUserStore.cs b/test/Microsoft.AspNetCore.Identity.InMemory.Test/InMemoryUserStore.cs index b71b22e785..de804bacf2 100644 --- a/test/Microsoft.AspNetCore.Identity.InMemory.Test/InMemoryUserStore.cs +++ b/test/Microsoft.AspNetCore.Identity.InMemory.Test/InMemoryUserStore.cs @@ -431,5 +431,15 @@ namespace Microsoft.AspNetCore.Identity.InMemory } return false; } + + public async Task CountCodesAsync(TUser user, CancellationToken cancellationToken) + { + var mergedCodes = await GetTokenAsync(user, AuthenticatorStoreLoginProvider, RecoveryCodeTokenName, cancellationToken) ?? ""; + if (mergedCodes.Length > 0) + { + return mergedCodes.Split(';').Length; + } + return 0; + } } }