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>
|
/// <summary>
|
||||||
/// Returns an <see cref="IdentityError"/> indicating a password entered does not contain a non-alphanumeric character, which is required by the password policy.
|
/// Returns an <see cref="IdentityError"/> indicating a password entered does not contain a non-alphanumeric character, which is required by the password policy.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,14 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public int RequiredLength { get; set; } = 6;
|
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>
|
/// <summary>
|
||||||
/// Gets or sets a flag indicating if passwords must contain a non-alphanumeric character.
|
/// Gets or sets a flag indicating if passwords must contain a non-alphanumeric character.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,10 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
{
|
{
|
||||||
errors.Add(Describer.PasswordRequiresUpper());
|
errors.Add(Describer.PasswordRequiresUpper());
|
||||||
}
|
}
|
||||||
|
if (options.RequiredUniqueChars >= 1 && password.Distinct().Count() < options.RequiredUniqueChars)
|
||||||
|
{
|
||||||
|
errors.Add(Describer.PasswordRequiresUniqueChars(options.RequiredUniqueChars));
|
||||||
|
}
|
||||||
return
|
return
|
||||||
Task.FromResult(errors.Count == 0
|
Task.FromResult(errors.Count == 0
|
||||||
? IdentityResult.Success
|
? IdentityResult.Success
|
||||||
|
|
|
||||||
|
|
@ -746,6 +746,22 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
return GetString("StoreNotIUserTwoFactorRecoveryCodeStore");
|
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)
|
private static string GetString(string name, params string[] formatterNames)
|
||||||
{
|
{
|
||||||
var value = _resourceManager.GetString(name);
|
var value = _resourceManager.GetString(name);
|
||||||
|
|
|
||||||
|
|
@ -301,4 +301,8 @@
|
||||||
<value>Store does not implement IUserTwoFactorRecoveryCodeStore<User>.</value>
|
<value>Store does not implement IUserTwoFactorRecoveryCodeStore<User>.</value>
|
||||||
<comment>Error when the store does not implement this interface</comment>
|
<comment>Error when the store does not implement this interface</comment>
|
||||||
</data>
|
</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>
|
</root>
|
||||||
|
|
@ -27,6 +27,7 @@ namespace Microsoft.AspNetCore.Identity.Test
|
||||||
Assert.True(options.Password.RequireNonAlphanumeric);
|
Assert.True(options.Password.RequireNonAlphanumeric);
|
||||||
Assert.True(options.Password.RequireUppercase);
|
Assert.True(options.Password.RequireUppercase);
|
||||||
Assert.Equal(6, options.Password.RequiredLength);
|
Assert.Equal(6, options.Password.RequiredLength);
|
||||||
|
Assert.Equal(1, options.Password.RequiredUniqueChars);
|
||||||
|
|
||||||
Assert.Equal("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+", options.User.AllowedUserNameCharacters);
|
Assert.Equal("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+", options.User.AllowedUserNameCharacters);
|
||||||
Assert.False(options.User.RequireUniqueEmail);
|
Assert.False(options.User.RequireUniqueEmail);
|
||||||
|
|
@ -58,6 +59,7 @@ namespace Microsoft.AspNetCore.Identity.Test
|
||||||
{"identity:claimsidentity:securitystampclaimtype", securityStampClaimType},
|
{"identity:claimsidentity:securitystampclaimtype", securityStampClaimType},
|
||||||
{"identity:user:requireUniqueEmail", "true"},
|
{"identity:user:requireUniqueEmail", "true"},
|
||||||
{"identity:password:RequiredLength", "10"},
|
{"identity:password:RequiredLength", "10"},
|
||||||
|
{"identity:password:RequiredUniqueChars", "5"},
|
||||||
{"identity:password:RequireNonAlphanumeric", "false"},
|
{"identity:password:RequireNonAlphanumeric", "false"},
|
||||||
{"identity:password:RequireUpperCase", "false"},
|
{"identity:password:RequireUpperCase", "false"},
|
||||||
{"identity:password:RequireDigit", "false"},
|
{"identity:password:RequireDigit", "false"},
|
||||||
|
|
@ -87,6 +89,7 @@ namespace Microsoft.AspNetCore.Identity.Test
|
||||||
Assert.False(options.Password.RequireNonAlphanumeric);
|
Assert.False(options.Password.RequireNonAlphanumeric);
|
||||||
Assert.False(options.Password.RequireUppercase);
|
Assert.False(options.Password.RequireUppercase);
|
||||||
Assert.Equal(10, options.Password.RequiredLength);
|
Assert.Equal(10, options.Password.RequiredLength);
|
||||||
|
Assert.Equal(5, options.Password.RequiredUniqueChars);
|
||||||
Assert.False(options.Lockout.AllowedForNewUsers);
|
Assert.False(options.Lockout.AllowedForNewUsers);
|
||||||
Assert.Equal(1000, options.Lockout.MaxFailedAccessAttempts);
|
Assert.Equal(1000, options.Lockout.MaxFailedAccessAttempts);
|
||||||
}
|
}
|
||||||
|
|
@ -129,6 +132,7 @@ namespace Microsoft.AspNetCore.Identity.Test
|
||||||
Assert.True(myOptions.Password.RequireDigit);
|
Assert.True(myOptions.Password.RequireDigit);
|
||||||
Assert.True(myOptions.Password.RequireNonAlphanumeric);
|
Assert.True(myOptions.Password.RequireNonAlphanumeric);
|
||||||
Assert.True(myOptions.Password.RequireUppercase);
|
Assert.True(myOptions.Password.RequireUppercase);
|
||||||
|
Assert.Equal(1, myOptions.Password.RequiredUniqueChars);
|
||||||
Assert.Equal(-1, myOptions.Password.RequiredLength);
|
Assert.Equal(-1, myOptions.Password.RequiredLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,44 @@ namespace Microsoft.AspNetCore.Identity.Test
|
||||||
IdentityResultAssert.IsSuccess(await valid.ValidateAsync(manager, null, input));
|
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]
|
[Theory]
|
||||||
[InlineData("abcde", Errors.Length | Errors.Alpha | Errors.Upper | Errors.Digit)]
|
[InlineData("abcde", Errors.Length | Errors.Alpha | Errors.Upper | Errors.Digit)]
|
||||||
[InlineData("a@B@cd", Errors.Digit)]
|
[InlineData("a@B@cd", Errors.Digit)]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue