|
namespace System.Web.Security {
using System;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web.Util;
/// <summary>
/// Validates whether a password field meets the current Membership Provider's password requirements.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
[SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "This attribute is designed to be a base class for other attributes which further want to customize password validation.")]
public class MembershipPasswordAttribute : ValidationAttribute {
#region Fields
private int? _minRequiredPasswordLength;
private int? _minRequiredNonAlphanumericCharacters;
private string _passwordStrengthRegularExpression;
private Type _resourceType;
private LocalizableString _minPasswordLengthError = new LocalizableString("MinPasswordLengthError");
private LocalizableString _minNonAlphanumericCharactersError = new LocalizableString("MinNonAlphanumericCharactersError");
private LocalizableString _passwordStrengthError = new LocalizableString("PasswordStrengthError");
#endregion
#region Properties
/// <summary>
/// Minimum required password length this attribute uses for validation.
/// If not explicitly set, defaults to <see cref="Membership.Provider.MinRequiredPasswordLength"/>.
/// </summary>
public int MinRequiredPasswordLength {
get {
return _minRequiredPasswordLength != null ? (int)_minRequiredPasswordLength : Membership.Provider.MinRequiredPasswordLength;
}
set {
_minRequiredPasswordLength = value;
}
}
/// <summary>
/// Minimum required non-alpha numeric characters this attribute uses for validation.
/// If not explicitly set, defaults to <see cref="Membership.Provider.MinRequiredNonAlphanumericCharacters"/>.
/// </summary>
public int MinRequiredNonAlphanumericCharacters {
get {
return _minRequiredNonAlphanumericCharacters != null ? (int)_minRequiredNonAlphanumericCharacters : Membership.Provider.MinRequiredNonAlphanumericCharacters;
}
set {
_minRequiredNonAlphanumericCharacters = value;
}
}
/// <summary>
/// Regular expression string representing the password strength this attribute uses for validation.
/// If not explicitly set, defaults to <see cref="Membership.Provider.PasswordStrengthRegularExpression"/>.
/// </summary>
public string PasswordStrengthRegularExpression {
get {
return _passwordStrengthRegularExpression ?? Membership.Provider.PasswordStrengthRegularExpression;
}
set {
_passwordStrengthRegularExpression = value;
}
}
/// <summary>
/// Gets or sets the <see cref="System.Type"/> that contains the resources for <see cref="MinPasswordLengthError"/>,
/// <see cref="MinNonAlphanumericCharactersError"/>, and <see cref="PasswordStrengthError"/>.
/// </summary>
public Type ResourceType {
get {
return this._resourceType;
}
set {
if (this._resourceType != value) {
this._resourceType = value;
this._minPasswordLengthError.ResourceType = value;
this._minNonAlphanumericCharactersError.ResourceType = value;
this._passwordStrengthError.ResourceType = value;
}
}
}
/// <summary>
/// Gets or sets the MinPasswordLengthError attribute property, which may be a resource key string.
/// </summary>
/// <remarks>
/// The property contains either the literal, non-localized string or the resource key
/// to be used in conjunction with <see cref="ResourceType"/> to configure the localized
/// error message displayed when the provided password is shorter than <see cref="Membership.Provider.MinRequiredPasswordLength"/>.
/// </remarks>
[SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "The property and method are a matched pair")]
public string MinPasswordLengthError {
get {
return this._minPasswordLengthError.Value;
}
set {
if (this._minPasswordLengthError.Value != value) {
this._minPasswordLengthError.Value = value;
}
}
}
/// <summary>
/// Gets or sets the MinNonAlphanumericCharactersError attribute property, which may be a resource key string.
/// </summary>
/// <remarks>
/// The property contains either the literal, non-localized string or the resource key
/// to be used in conjunction with <see cref="ResourceType"/> to configure the localized
/// error message displayed when the provided password contains less number of non-alphanumeric characters than
/// <see cref="Membership.Provider.MinRequiredNonAlphanumericCharacters"/>
/// </remarks>
[SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "The property and method are a matched pair")]
public string MinNonAlphanumericCharactersError {
get {
return this._minNonAlphanumericCharactersError.Value;
}
set {
if (this._minNonAlphanumericCharactersError.Value != value) {
this._minNonAlphanumericCharactersError.Value = value;
}
}
}
/// <summary>
/// Gets or sets the PasswordStrengthError attribute property, which may be a resource key string.
/// </summary>
/// <remarks>
/// The property contains either the literal, non-localized string or the resource key
/// to be used in conjunction with <see cref="ResourceType"/> to configure the localized
/// error message displayed when the provided password is shorter than <see cref="Membership.Provider.MinRequiredPasswordLength"/>.
/// </remarks>
[SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "The property and method are a matched pair")]
public string PasswordStrengthError {
get {
return this._passwordStrengthError.Value;
}
set {
if (this._passwordStrengthError.Value != value) {
this._passwordStrengthError.Value = value;
}
}
}
// The timeout for the regex we use to check password strength
public int? PasswordStrengthRegexTimeout { get; set; }
#endregion
#region Overriden Methods
/// <summary>
/// Overrider of <see cref="ValidationAttribute.IsValid(object,validationContext)"/>.
/// </summary>
/// <remarks>
/// Checks if the given value meets the password requirements such as minimum length, minimum number of non-alpha numeric characters
/// and password strength regular expression set in current <see cref="Membership.Provider"/>
/// </remarks>
/// <param name="value">The value to validate.</param>
/// <param name="validationContext">A <see cref="ValidationContext"/> instance that provides
/// context about the validation operation, such as the object and member being validated.</param>
/// <returns>
/// When validation is valid, <see cref="ValidationResult.Success"/>.
/// <para>
/// When validation is invalid, an instance of <see cref="ValidationResult"/>.
/// </para>
/// </returns>
protected override ValidationResult IsValid(object value, ValidationContext validationContext) {
string valueAsString = value as string;
string name = (validationContext != null) ? validationContext.DisplayName : String.Empty;
string[] memberNames = (validationContext != null) ? new[] { validationContext.MemberName } : null;
string errorMessage;
if (String.IsNullOrEmpty(valueAsString)) {
return ValidationResult.Success;
}
if (valueAsString.Length < MinRequiredPasswordLength) {
errorMessage = GetMinPasswordLengthError();
return new ValidationResult(FormatErrorMessage(errorMessage, name, MinRequiredPasswordLength), memberNames);
}
int nonAlphanumericCharacters = valueAsString.Count(c => !Char.IsLetterOrDigit(c));
if (nonAlphanumericCharacters < MinRequiredNonAlphanumericCharacters) {
errorMessage = GetMinNonAlphanumericCharactersError();
return new ValidationResult(FormatErrorMessage(errorMessage, name, MinRequiredNonAlphanumericCharacters), memberNames);
}
string passwordStrengthRegularExpression = PasswordStrengthRegularExpression;
if (passwordStrengthRegularExpression != null) {
Regex passwordStrengthRegex;
try {
// Adding timeout for Regex in case of malicious string causing DoS
passwordStrengthRegex = RegexUtil.CreateRegex(passwordStrengthRegularExpression, RegexOptions.None, PasswordStrengthRegexTimeout);
}
catch (ArgumentException ex) {
throw new InvalidOperationException(SR.GetString(SR.MembershipPasswordAttribute_InvalidRegularExpression), ex);
}
if (!passwordStrengthRegex.IsMatch(valueAsString)) {
errorMessage = GetPasswordStrengthError();
return new ValidationResult(FormatErrorMessage(errorMessage, name, additionalArgument: String.Empty), memberNames);
}
}
return ValidationResult.Success;
}
public override string FormatErrorMessage(string name) {
return FormatErrorMessage(errorMessageString: ErrorMessageString, name: name, additionalArgument: String.Empty);
}
#endregion
#region Private Methods
/// <summary>
/// Gets the error message string shown when the provided password is shorter than <see cref="Membership.Provider.MinRequiredPasswordLength"/>.
/// <para>
/// This can be either a literal, non-localized string provided to <see cref="MinPasswordLengthError"/> or the
/// localized string found when <see cref="ResourceType"/> has been specified and <see cref="MinPasswordLengthError"/>
/// represents a resource key within that resource type.
/// </para>
/// </summary>
/// <returns>
/// When <see cref="ResourceType"/> has not been specified, the value of
/// <see cref="MinPasswordLengthError"/> will be returned.
/// <para>
/// When <see cref="ResourceType"/> has been specified and <see cref="MinPasswordLengthError"/>
/// represents a resource key within that resource type, then the localized value will be returned.
/// </para>
/// <para>
/// When <see cref="MinPasswordLengthError"/> has not been specified, a default error message will be returned.
/// </para>
/// </returns>
/// <exception cref="System.InvalidOperationException">
/// After setting both the <see cref="ResourceType"/> property and the <see cref="MinPasswordLengthError"/> property,
/// but a public static property with a name matching the <see cref="MinPasswordLengthError"/> value couldn't be found
/// on the <see cref="ResourceType"/>.
/// </exception>
[SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This method does work using a property of the same name")]
private string GetMinPasswordLengthError() {
return this._minPasswordLengthError.GetLocalizableValue() ?? SR.GetString(SR.MembershipPasswordAttribute_InvalidPasswordLength);
}
/// <summary>
/// Gets the error message string shown when the provided password contains less number of non-alphanumeric characters than
/// <see cref="Membership.Provider.MinRequiredNonAlphanumericCharacters"/>
/// <para>
/// This can be either a literal, non-localized string provided to <see cref="MinNonAlphanumericCharactersError"/> or the
/// localized string found when <see cref="ResourceType"/> has been specified and <see cref="MinNonAlphanumericCharactersError"/>
/// represents a resource key within that resource type.
/// </para>
/// </summary>
/// <returns>
/// When <see cref="ResourceType"/> has not been specified, the value of
/// <see cref="MinNonAlphanumericCharactersError"/> will be returned.
/// <para>
/// When <see cref="ResourceType"/> has been specified and <see cref="MinNonAlphanumericCharactersError"/>
/// represents a resource key within that resource type, then the localized value will be returned.
/// </para>
/// <para>
/// When <see cref="MinNonAlphanumericCharactersError"/> has not been specified, a default error message will be returned.
/// </para>
/// </returns>
/// <exception cref="System.InvalidOperationException">
/// After setting both the <see cref="ResourceType"/> property and the <see cref="MinNonAlphanumericCharactersError"/> property,
/// but a public static property with a name matching the <see cref="MinNonAlphanumericCharactersError"/> value couldn't be found
/// on the <see cref="ResourceType"/>.
/// </exception>
[SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This method does work using a property of the same name")]
private string GetMinNonAlphanumericCharactersError() {
return this._minNonAlphanumericCharactersError.GetLocalizableValue() ?? SR.GetString(SR.MembershipPasswordAttribute_InvalidPasswordNonAlphanumericCharacters);
}
/// <summary>
/// Gets the error message string shown when the provided password is shorter than <see cref="Membership.Provider.MinRequiredPasswordLength"/>.
/// <para>
/// This can be either a literal, non-localized string provided to <see cref="PasswordStrengthError"/> or the
/// localized string found when <see cref="ResourceType"/> has been specified and <see cref="PasswordStrengthError"/>
/// represents a resource key within that resource type.
/// </para>
/// </summary>
/// <returns>
/// When <see cref="ResourceType"/> has not been specified, the value of
/// <see cref="PasswordStrengthError"/> will be returned.
/// <para>
/// When <see cref="ResourceType"/> has been specified and <see cref="PasswordStrengthError"/>
/// represents a resource key within that resource type, then the localized value will be returned.
/// </para>
/// <para>
/// When <see cref="PasswordStrengthError"/> has not been specified, a default error message will be returned.
/// </para>
/// </returns>
/// <exception cref="System.InvalidOperationException">
/// After setting both the <see cref="ResourceType"/> property and the <see cref="PasswordStrengthError"/> property,
/// but a public static property with a name matching the <see cref="PasswordStrengthError"/> value couldn't be found
/// on the <see cref="ResourceType"/>.
/// </exception>
[SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This method does work using a property of the same name")]
private string GetPasswordStrengthError() {
return this._passwordStrengthError.GetLocalizableValue() ?? SR.GetString(SR.MembershipPasswordAttribute_InvalidPasswordStrength);
}
private string FormatErrorMessage(string errorMessageString, string name, object additionalArgument) {
return String.Format(CultureInfo.CurrentCulture, errorMessageString, name, additionalArgument);
}
#endregion
}
}
|