Implementing PasswordOptions.RequiredUniqueChars (#1100)

Fixes #1097
This commit is contained in:
Sébastien Ros 2017-02-10 18:15:16 -08:00 committed by GitHub
parent da27614293
commit addb9a45cb
7 changed files with 88 additions and 0 deletions

View File

@ -241,6 +241,20 @@ namespace Microsoft.AspNetCore.Identity
};
}
/// <summary>
/// Returns an <see cref="IdentityError"/> indicating a password does not meet the minimum number <paramref name="uniqueChars"/> of unique chars.
/// </summary>
/// <param name="uniqueChars">The number of different chars that must be used.</param>
/// <returns>An <see cref="IdentityError"/> indicating a password does not meet the minimum number <paramref name="uniqueChars"/> of unique chars.</returns>
public virtual IdentityError PasswordRequiresUniqueChars(int uniqueChars)
{
return new IdentityError
{
Code = nameof(PasswordRequiresUniqueChars),
Description = Resources.FormatPasswordRequiresUniqueChars(uniqueChars)
};
}
/// <summary>
/// Returns an <see cref="IdentityError"/> indicating a password entered does not contain a non-alphanumeric character, which is required by the password policy.
/// </summary>

View File

@ -16,6 +16,14 @@ namespace Microsoft.AspNetCore.Identity
/// </remarks>
public int RequiredLength { get; set; } = 6;
/// <summary>
/// Gets or sets the minimum number of unique chars a password must comprised of.
/// </summary>
/// <remarks>
/// This defaults to 1.
/// </remarks>
public int RequiredUniqueChars { get; set; } = 1;
/// <summary>
/// Gets or sets a flag indicating if passwords must contain a non-alphanumeric character.
/// </summary>

View File

@ -68,6 +68,10 @@ namespace Microsoft.AspNetCore.Identity
{
errors.Add(Describer.PasswordRequiresUpper());
}
if (options.RequiredUniqueChars >= 1 && password.Distinct().Count() < options.RequiredUniqueChars)
{
errors.Add(Describer.PasswordRequiresUniqueChars(options.RequiredUniqueChars));
}
return
Task.FromResult(errors.Count == 0
? IdentityResult.Success

View File

@ -746,6 +746,22 @@ namespace Microsoft.AspNetCore.Identity
return GetString("StoreNotIUserTwoFactorRecoveryCodeStore");
}
/// <summary>
/// Passwords must use at least {0} different characters.
/// </summary>
internal static string PasswordRequiresUniqueChars
{
get { return GetString("PasswordRequiresUniqueChars"); }
}
/// <summary>
/// Passwords must use at least {0} different characters.
/// </summary>
internal static string FormatPasswordRequiresUniqueChars(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("PasswordRequiresUniqueChars"), p0);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -301,4 +301,8 @@
<value>Store does not implement IUserTwoFactorRecoveryCodeStore&lt;User&gt;.</value>
<comment>Error when the store does not implement this interface</comment>
</data>
<data name="PasswordRequiresUniqueChars" xml:space="preserve">
<value>Passwords must use at least {0} different characters.</value>
<comment>Error message for passwords that are based on similar characters</comment>
</data>
</root>

View File

@ -27,6 +27,7 @@ namespace Microsoft.AspNetCore.Identity.Test
Assert.True(options.Password.RequireNonAlphanumeric);
Assert.True(options.Password.RequireUppercase);
Assert.Equal(6, options.Password.RequiredLength);
Assert.Equal(1, options.Password.RequiredUniqueChars);
Assert.Equal("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+", options.User.AllowedUserNameCharacters);
Assert.False(options.User.RequireUniqueEmail);
@ -58,6 +59,7 @@ namespace Microsoft.AspNetCore.Identity.Test
{"identity:claimsidentity:securitystampclaimtype", securityStampClaimType},
{"identity:user:requireUniqueEmail", "true"},
{"identity:password:RequiredLength", "10"},
{"identity:password:RequiredUniqueChars", "5"},
{"identity:password:RequireNonAlphanumeric", "false"},
{"identity:password:RequireUpperCase", "false"},
{"identity:password:RequireDigit", "false"},
@ -87,6 +89,7 @@ namespace Microsoft.AspNetCore.Identity.Test
Assert.False(options.Password.RequireNonAlphanumeric);
Assert.False(options.Password.RequireUppercase);
Assert.Equal(10, options.Password.RequiredLength);
Assert.Equal(5, options.Password.RequiredUniqueChars);
Assert.False(options.Lockout.AllowedForNewUsers);
Assert.Equal(1000, options.Lockout.MaxFailedAccessAttempts);
}
@ -129,6 +132,7 @@ namespace Microsoft.AspNetCore.Identity.Test
Assert.True(myOptions.Password.RequireDigit);
Assert.True(myOptions.Password.RequireNonAlphanumeric);
Assert.True(myOptions.Password.RequireUppercase);
Assert.Equal(1, myOptions.Password.RequiredUniqueChars);
Assert.Equal(-1, myOptions.Password.RequiredLength);
}

View File

@ -97,6 +97,44 @@ namespace Microsoft.AspNetCore.Identity.Test
IdentityResultAssert.IsSuccess(await valid.ValidateAsync(manager, null, input));
}
[Theory]
[InlineData("a", 2)]
[InlineData("aaaaaaaaaaa", 2)]
[InlineData("abcdabcdabcdabcdabcdabcdabcd", 5)]
public async Task FailsWithoutRequiredUniqueCharsTests(string input, int uniqueChars)
{
var manager = MockHelpers.TestUserManager<TestUser>();
var valid = new PasswordValidator<TestUser>();
manager.Options.Password.RequireUppercase = false;
manager.Options.Password.RequireNonAlphanumeric = false;
manager.Options.Password.RequireLowercase = false;
manager.Options.Password.RequireDigit = false;
manager.Options.Password.RequiredLength = 0;
manager.Options.Password.RequiredUniqueChars = uniqueChars;
IdentityResultAssert.IsFailure(await valid.ValidateAsync(manager, null, input),
String.Format("Passwords must use at least {0} different characters.", uniqueChars));
}
[Theory]
[InlineData("12345", 5)]
[InlineData("aAbBc", 5)]
[InlineData("aAbBcaAbBcaAbBc", 5)]
[InlineData("!@#$%", 5)]
[InlineData("a", 1)]
[InlineData("this is a long password with many chars", 10)]
public async Task SucceedsWithRequiredUniqueCharsTests(string input, int uniqueChars)
{
var manager = MockHelpers.TestUserManager<TestUser>();
var valid = new PasswordValidator<TestUser>();
manager.Options.Password.RequireUppercase = false;
manager.Options.Password.RequireNonAlphanumeric = false;
manager.Options.Password.RequireLowercase = false;
manager.Options.Password.RequireDigit = false;
manager.Options.Password.RequiredLength = 0;
manager.Options.Password.RequiredUniqueChars = uniqueChars;
IdentityResultAssert.IsSuccess(await valid.ValidateAsync(manager, null, input));
}
[Theory]
[InlineData("abcde", Errors.Length | Errors.Alpha | Errors.Upper | Errors.Digit)]
[InlineData("a@B@cd", Errors.Digit)]