using System; using System.Collections.Specialized; using System.Configuration.Provider; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Web.Configuration; using System.Web.Hosting; using System.Web.Security; using Umbraco.Core.Logging; namespace Umbraco.Core.Security { /// /// A base membership provider class offering much of the underlying functionality for initializing and password encryption/hashing. /// public abstract class MembershipProviderBase : MembershipProvider { /// /// Providers can override this setting, default is 7 /// public virtual int DefaultMinPasswordLength { get { return 7; } } /// /// Providers can override this setting, default is 1 /// public virtual int DefaultMinNonAlphanumericChars { get { return 1; } } /// /// Providers can override this setting, default is false to use better security /// public virtual bool DefaultUseLegacyEncoding { get { return false; } } /// /// Providers can override this setting, by default this is false which means that the provider will /// authenticate the username + password when ChangePassword is called. This property exists purely for /// backwards compatibility. /// public virtual bool AllowManuallyChangingPassword { get { return false; } } private string _applicationName; private bool _enablePasswordReset; private bool _enablePasswordRetrieval; private int _maxInvalidPasswordAttempts; private int _minRequiredNonAlphanumericCharacters; private int _minRequiredPasswordLength; private int _passwordAttemptWindow; private MembershipPasswordFormat _passwordFormat; private string _passwordStrengthRegularExpression; private bool _requiresQuestionAndAnswer; private bool _requiresUniqueEmail; private string _customHashAlgorithmType ; internal bool UseLegacyEncoding; #region Properties /// /// Indicates whether the membership provider is configured to allow users to reset their passwords. /// /// /// true if the membership provider supports password reset; otherwise, false. The default is true. public override bool EnablePasswordReset { get { return _enablePasswordReset; } } /// /// Indicates whether the membership provider is configured to allow users to retrieve their passwords. /// /// /// true if the membership provider is configured to support password retrieval; otherwise, false. The default is false. public override bool EnablePasswordRetrieval { get { return _enablePasswordRetrieval; } } /// /// Gets the number of invalid password or password-answer attempts allowed before the membership user is locked out. /// /// /// The number of invalid password or password-answer attempts allowed before the membership user is locked out. public override int MaxInvalidPasswordAttempts { get { return _maxInvalidPasswordAttempts; } } /// /// Gets the minimum number of special characters that must be present in a valid password. /// /// /// The minimum number of special characters that must be present in a valid password. public override int MinRequiredNonAlphanumericCharacters { get { return _minRequiredNonAlphanumericCharacters; } } /// /// Gets the minimum length required for a password. /// /// /// The minimum length required for a password. public override int MinRequiredPasswordLength { get { return _minRequiredPasswordLength; } } /// /// Gets the number of minutes in which a maximum number of invalid password or password-answer attempts are allowed before the membership user is locked out. /// /// /// The number of minutes in which a maximum number of invalid password or password-answer attempts are allowed before the membership user is locked out. public override int PasswordAttemptWindow { get { return _passwordAttemptWindow; } } /// /// Gets a value indicating the format for storing passwords in the membership data store. /// /// /// One of the values indicating the format for storing passwords in the data store. public override MembershipPasswordFormat PasswordFormat { get { return _passwordFormat; } } /// /// Gets the regular expression used to evaluate a password. /// /// /// A regular expression used to evaluate a password. public override string PasswordStrengthRegularExpression { get { return _passwordStrengthRegularExpression; } } /// /// Gets a value indicating whether the membership provider is configured to require the user to answer a password question for password reset and retrieval. /// /// /// true if a password answer is required for password reset and retrieval; otherwise, false. The default is true. public override bool RequiresQuestionAndAnswer { get { return _requiresQuestionAndAnswer; } } /// /// Gets a value indicating whether the membership provider is configured to require a unique e-mail address for each user name. /// /// /// true if the membership provider requires a unique e-mail address; otherwise, false. The default is true. public override bool RequiresUniqueEmail { get { return _requiresUniqueEmail; } } /// /// The name of the application using the custom membership provider. /// /// /// The name of the application using the custom membership provider. public override string ApplicationName { get { return _applicationName; } set { if (string.IsNullOrEmpty(value)) throw new ProviderException("ApplicationName cannot be empty."); if (value.Length > 0x100) throw new ProviderException("Provider application name too long."); _applicationName = value; } } #endregion /// /// Initializes the provider. /// /// The friendly name of the provider. /// A collection of the name/value pairs representing the provider-specific attributes specified in the configuration for this provider. /// The name of the provider is null. /// An attempt is made to call /// on a provider after the provider /// has already been initialized. /// The name of the provider has a length of zero. public override void Initialize(string name, NameValueCollection config) { // Initialize base provider class base.Initialize(name, config); _enablePasswordRetrieval = config.GetValue("enablePasswordRetrieval", false); _enablePasswordReset = config.GetValue("enablePasswordReset", false); _requiresQuestionAndAnswer = config.GetValue("requiresQuestionAndAnswer", false); _requiresUniqueEmail = config.GetValue("requiresUniqueEmail", true); _maxInvalidPasswordAttempts = GetIntValue(config, "maxInvalidPasswordAttempts", 5, false, 0); _passwordAttemptWindow = GetIntValue(config, "passwordAttemptWindow", 10, false, 0); _minRequiredPasswordLength = GetIntValue(config, "minRequiredPasswordLength", DefaultMinPasswordLength, true, 0x80); _minRequiredNonAlphanumericCharacters = GetIntValue(config, "minRequiredNonalphanumericCharacters", DefaultMinNonAlphanumericChars, true, 0x80); _passwordStrengthRegularExpression = config["passwordStrengthRegularExpression"]; _applicationName = config["applicationName"]; if (string.IsNullOrEmpty(_applicationName)) _applicationName = GetDefaultAppName(); //by default we will continue using the legacy encoding. UseLegacyEncoding = config.GetValue("useLegacyEncoding", DefaultUseLegacyEncoding); // make sure password format is Hashed by default. string str = config["passwordFormat"] ?? "Hashed"; switch (str.ToLower()) { case "clear": _passwordFormat = MembershipPasswordFormat.Clear; break; case "encrypted": _passwordFormat = MembershipPasswordFormat.Encrypted; break; case "hashed": _passwordFormat = MembershipPasswordFormat.Hashed; break; default: throw new ProviderException("Provider bad password format"); } if ((PasswordFormat == MembershipPasswordFormat.Hashed) && EnablePasswordRetrieval) { var ex = new ProviderException("Provider can not retrieve a hashed password"); LogHelper.Error("Cannot specify a Hashed password format with the enabledPasswordRetrieval option set to true", ex); throw ex; } _customHashAlgorithmType = config.GetValue("hashAlgorithmType", string.Empty); } /// /// Override this method to ensure the password is valid before raising the event /// /// protected override void OnValidatingPassword(ValidatePasswordEventArgs e) { var attempt = IsPasswordValid(e.Password, MinRequiredNonAlphanumericCharacters, PasswordStrengthRegularExpression, MinRequiredPasswordLength); if (attempt.Success == false) { e.Cancel = true; return; } base.OnValidatingPassword(e); } protected internal enum PasswordValidityError { Ok, Length, AlphanumericChars, Strength } /// /// Processes a request to update the password for a membership user. /// /// The user to update the password for. /// This property is ignore for this provider /// The new password for the specified user. /// /// true if the password was updated successfully; otherwise, false. /// /// /// Checks to ensure the AllowManuallyChangingPassword rule is adhered to /// public override bool ChangePassword(string username, string oldPassword, string newPassword) { if (oldPassword.IsNullOrWhiteSpace() && AllowManuallyChangingPassword == false) { //If the old password is empty and AllowManuallyChangingPassword is false, than this provider cannot just arbitrarily change the password throw new NotSupportedException("This provider does not support manually changing the password"); } var args = new ValidatePasswordEventArgs(username, newPassword, false); OnValidatingPassword(args); if (args.Cancel) { if (args.FailureInformation != null) throw args.FailureInformation; throw new MembershipPasswordException("Change password canceled due to password validation failure."); } if (AllowManuallyChangingPassword == false) { if (ValidateUser(username, oldPassword) == false) return false; } return PerformChangePassword(username, oldPassword, newPassword); } /// /// Processes a request to update the password for a membership user. /// /// The user to update the password for. /// This property is ignore for this provider /// The new password for the specified user. /// /// true if the password was updated successfully; otherwise, false. /// protected abstract bool PerformChangePassword(string username, string oldPassword, string newPassword); /// /// Processes a request to update the password question and answer for a membership user. /// /// The user to change the password question and answer for. /// The password for the specified user. /// The new password question for the specified user. /// The new password answer for the specified user. /// /// true if the password question and answer are updated successfully; otherwise, false. /// /// /// Performs the basic validation before passing off to PerformChangePasswordQuestionAndAnswer /// public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer) { if (RequiresQuestionAndAnswer == false) { throw new NotSupportedException("Updating the password Question and Answer is not available if requiresQuestionAndAnswer is not set in web.config"); } if (AllowManuallyChangingPassword == false) { if (ValidateUser(username, password) == false) { return false; } } return PerformChangePasswordQuestionAndAnswer(username, password, newPasswordQuestion, newPasswordAnswer); } /// /// Processes a request to update the password question and answer for a membership user. /// /// The user to change the password question and answer for. /// The password for the specified user. /// The new password question for the specified user. /// The new password answer for the specified user. /// /// true if the password question and answer are updated successfully; otherwise, false. /// protected abstract bool PerformChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer); /// /// Adds a new membership user to the data source. /// /// The user name for the new user. /// The password for the new user. /// The e-mail address for the new user. /// The password question for the new user. /// The password answer for the new user /// Whether or not the new user is approved to be validated. /// The unique identifier from the membership data source for the user. /// A enumeration value indicating whether the user was created successfully. /// /// A object populated with the information for the newly created user. /// /// /// Ensures the ValidatingPassword event is executed before executing PerformCreateUser and performs basic membership provider validation of values. /// public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status) { var valStatus = ValidateNewUser(username, password, email, passwordQuestion, passwordAnswer, isApproved, providerUserKey); if (valStatus != MembershipCreateStatus.Success) { status = valStatus; return null; } return PerformCreateUser(username, password, email, passwordQuestion, passwordAnswer, isApproved, providerUserKey, out status); } /// /// Performs the validation of the information for creating a new user /// /// /// /// /// /// /// /// /// protected MembershipCreateStatus ValidateNewUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey) { var args = new ValidatePasswordEventArgs(username, password, true); OnValidatingPassword(args); if (args.Cancel) { return MembershipCreateStatus.InvalidPassword; } // Validate password var passwordValidAttempt = IsPasswordValid(password, MinRequiredNonAlphanumericCharacters, PasswordStrengthRegularExpression, MinRequiredPasswordLength); if (passwordValidAttempt.Success == false) { return MembershipCreateStatus.InvalidPassword; } // Validate email if (IsEmailValid(email) == false) { return MembershipCreateStatus.InvalidEmail; } // Make sure username isn't all whitespace if (string.IsNullOrWhiteSpace(username.Trim())) { return MembershipCreateStatus.InvalidUserName; } // Check password question if (string.IsNullOrWhiteSpace(passwordQuestion) && RequiresQuestionAndAnswer) { return MembershipCreateStatus.InvalidQuestion; } // Check password answer if (string.IsNullOrWhiteSpace(passwordAnswer) && RequiresQuestionAndAnswer) { return MembershipCreateStatus.InvalidAnswer; } return MembershipCreateStatus.Success; } /// /// Adds a new membership user to the data source. /// /// The user name for the new user. /// The password for the new user. /// The e-mail address for the new user. /// The password question for the new user. /// The password answer for the new user /// Whether or not the new user is approved to be validated. /// The unique identifier from the membership data source for the user. /// A enumeration value indicating whether the user was created successfully. /// /// A object populated with the information for the newly created user. /// protected abstract MembershipUser PerformCreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status); /// /// Gets the members password if password retreival is enabled /// /// /// /// public override string GetPassword(string username, string answer) { if (EnablePasswordRetrieval == false) throw new ProviderException("Password Retrieval Not Enabled."); if (PasswordFormat == MembershipPasswordFormat.Hashed) throw new ProviderException("Cannot retrieve Hashed passwords."); return PerformGetPassword(username, answer); } /// /// Gets the members password if password retreival is enabled /// /// /// /// protected abstract string PerformGetPassword(string username, string answer); public override string ResetPassword(string username, string answer) { if (EnablePasswordReset == false) { throw new NotSupportedException("Password reset is not supported"); } var newPassword = Membership.GeneratePassword(MinRequiredPasswordLength, MinRequiredNonAlphanumericCharacters); var args = new ValidatePasswordEventArgs(username, newPassword, true); OnValidatingPassword(args); if (args.Cancel) { if (args.FailureInformation != null) { throw args.FailureInformation; } throw new MembershipPasswordException("Reset password canceled due to password validation failure."); } return PerformResetPassword(username, answer, newPassword); } protected abstract string PerformResetPassword(string username, string answer, string generatedPassword); protected internal static Attempt IsPasswordValid(string password, int minRequiredNonAlphanumericChars, string strengthRegex, int minLength) { if (minRequiredNonAlphanumericChars > 0) { var nonAlphaNumeric = Regex.Replace(password, "[a-zA-Z0-9]", "", RegexOptions.Multiline | RegexOptions.IgnoreCase); if (nonAlphaNumeric.Length < minRequiredNonAlphanumericChars) { return Attempt.Fail(PasswordValidityError.AlphanumericChars); } } if (string.IsNullOrEmpty(strengthRegex) == false) { if (Regex.IsMatch(password, strengthRegex, RegexOptions.Compiled) == false) { return Attempt.Fail(PasswordValidityError.Strength); } } if (password.Length < minLength) { return Attempt.Fail(PasswordValidityError.Length); } return Attempt.Succeed(PasswordValidityError.Ok); } /// /// Gets the name of the default app. /// /// internal static string GetDefaultAppName() { try { string applicationVirtualPath = HostingEnvironment.ApplicationVirtualPath; if (string.IsNullOrEmpty(applicationVirtualPath)) { return "/"; } return applicationVirtualPath; } catch { return "/"; } } internal static int GetIntValue(NameValueCollection config, string valueName, int defaultValue, bool zeroAllowed, int maxValueAllowed) { int num; string s = config[valueName]; if (s == null) { return defaultValue; } if (!int.TryParse(s, out num)) { if (zeroAllowed) { throw new ProviderException("Value must be non negative integer"); } throw new ProviderException("Value must be positive integer"); } if (zeroAllowed && (num < 0)) { throw new ProviderException("Value must be non negativeinteger"); } if (!zeroAllowed && (num <= 0)) { throw new ProviderException("Value must be positive integer"); } if ((maxValueAllowed > 0) && (num > maxValueAllowed)) { throw new ProviderException("Value too big"); } return num; } /// /// If the password format is a hashed keyed algorithm then we will pre-pend the salt used to hash the password /// to the hashed password itself. /// /// /// /// protected internal string FormatPasswordForStorage(string pass, string salt) { if (UseLegacyEncoding) { return pass; } if (PasswordFormat == MembershipPasswordFormat.Hashed) { //the better way, we use salt per member return salt + pass; } return pass; } internal static bool IsEmailValid(string email) { const string pattern = @"^(?!\.)(""([^""\r\\]|\\[""\r\\])*""|" + @"([-a-z0-9!#$%&'*+/=?^_`{|}~]|(? /// Checks the password. /// /// The password. /// The dbPassword. /// protected internal bool CheckPassword(string password, string dbPassword) { switch (PasswordFormat) { case MembershipPasswordFormat.Encrypted: var decrypted = DecryptPassword(dbPassword); return decrypted == password; case MembershipPasswordFormat.Hashed: string salt; var storedHashedPass = StoredPassword(dbPassword, out salt); var hashed = EncryptOrHashPassword(password, salt); return storedHashedPass == hashed; case MembershipPasswordFormat.Clear: return password == dbPassword; default: throw new ArgumentOutOfRangeException(); } } /// /// Encrypt/hash a new password with a new salt /// /// /// /// protected internal string EncryptOrHashNewPassword(string newPassword, out string salt) { salt = GenerateSalt(); return EncryptOrHashPassword(newPassword, salt); } protected internal string DecryptPassword(string pass) { //if we are doing it the old way if (UseLegacyEncoding) { return LegacyUnEncodePassword(pass); } //This is the correct way to implement this (as per the sql membership provider) switch ((int)PasswordFormat) { case 0: return pass; case 1: throw new ProviderException("Provider can not decrypt hashed password"); default: var bytes = DecryptPassword(Convert.FromBase64String(pass)); return bytes == null ? null : Encoding.Unicode.GetString(bytes, 16, bytes.Length - 16); } } /// /// Returns the hashed password without the salt if it is hashed /// /// /// returns the salt /// internal string StoredPassword(string storedString, out string salt) { if (UseLegacyEncoding) { salt = string.Empty; return storedString; } switch (PasswordFormat) { case MembershipPasswordFormat.Hashed: var saltLen = GenerateSalt(); salt = storedString.Substring(0, saltLen.Length); return storedString.Substring(saltLen.Length); case MembershipPasswordFormat.Clear: case MembershipPasswordFormat.Encrypted: default: salt = string.Empty; return storedString; } } protected internal static string GenerateSalt() { var numArray = new byte[16]; new RNGCryptoServiceProvider().GetBytes(numArray); return Convert.ToBase64String(numArray); } protected internal HashAlgorithm GetHashAlgorithm(string password) { if (UseLegacyEncoding) { //before we were never checking for an algorithm type so we were always using HMACSHA1 // for any SHA specified algorithm :( so we'll need to keep doing that for backwards compat support. if (Membership.HashAlgorithmType.InvariantContains("SHA")) { return new HMACSHA1 { //the legacy salt was actually the password :( Key = Encoding.Unicode.GetBytes(password) }; } } //get the algorithm by name if (_customHashAlgorithmType.IsNullOrWhiteSpace()) { _customHashAlgorithmType = Membership.HashAlgorithmType; } var alg = HashAlgorithm.Create(_customHashAlgorithmType); if (alg == null) { throw new InvalidOperationException("The hash algorithm specified " + Membership.HashAlgorithmType + " cannot be resolved"); } return alg; } /// /// Encodes the password. /// /// The password. /// The encoded password. protected string LegacyEncodePassword(string password) { string encodedPassword = password; switch (PasswordFormat) { case MembershipPasswordFormat.Clear: break; case MembershipPasswordFormat.Encrypted: encodedPassword = Convert.ToBase64String(EncryptPassword(Encoding.Unicode.GetBytes(password))); break; case MembershipPasswordFormat.Hashed: var hashAlgorith = GetHashAlgorithm(password); encodedPassword = Convert.ToBase64String(hashAlgorith.ComputeHash(Encoding.Unicode.GetBytes(password))); break; default: throw new ProviderException("Unsupported password format."); } return encodedPassword; } /// /// Unencode password. /// /// The encoded password. /// The unencoded password. protected string LegacyUnEncodePassword(string encodedPassword) { string password = encodedPassword; switch (PasswordFormat) { case MembershipPasswordFormat.Clear: break; case MembershipPasswordFormat.Encrypted: password = Encoding.Unicode.GetString(DecryptPassword(Convert.FromBase64String(password))); break; case MembershipPasswordFormat.Hashed: throw new ProviderException("Cannot unencode a hashed password."); default: throw new ProviderException("Unsupported password format."); } return password; } public override string ToString() { var result = base.ToString(); var sb = new StringBuilder(result); sb.AppendLine("Name =" + Name); sb.AppendLine("_applicationName =" + _applicationName); sb.AppendLine("_enablePasswordReset=" + _enablePasswordReset); sb.AppendLine("_enablePasswordRetrieval=" + _enablePasswordRetrieval); sb.AppendLine("_maxInvalidPasswordAttempts=" + _maxInvalidPasswordAttempts); sb.AppendLine("_minRequiredNonAlphanumericCharacters=" + _minRequiredNonAlphanumericCharacters); sb.AppendLine("_minRequiredPasswordLength=" + _minRequiredPasswordLength); sb.AppendLine("_passwordAttemptWindow=" + _passwordAttemptWindow); sb.AppendLine("_passwordFormat=" + _passwordFormat); sb.AppendLine("_passwordStrengthRegularExpression=" + _passwordStrengthRegularExpression); sb.AppendLine("_requiresQuestionAndAnswer=" + _requiresQuestionAndAnswer); sb.AppendLine("_requiresUniqueEmail=" + _requiresUniqueEmail); return sb.ToString(); } } }