parent
da27614293
commit
addb9a45cb
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -301,4 +301,8 @@
|
|||
<value>Store does not implement IUserTwoFactorRecoveryCodeStore<User>.</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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
Loading…
Reference in New Issue