using System; using System.Collections.Specialized; using System.ComponentModel.DataAnnotations; using System.Configuration.Provider; using System.Text; using System.Text.RegularExpressions; using System.Web; using System.Web.Hosting; using System.Web.Configuration; using System.Web.Security; using Umbraco.Core; using Umbraco.Web.Composing; using Umbraco.Core.Hosting; using Umbraco.Core.Logging; using Umbraco.Core.Security; namespace Umbraco.Web.Security { /// /// A base membership provider class offering much of the underlying functionality for initializing and password encryption/hashing. /// public abstract class MembershipProviderBase : MembershipProvider { private readonly IHostingEnvironment _hostingEnvironment; protected MembershipProviderBase(IHostingEnvironment hostingEnvironment) { _hostingEnvironment = hostingEnvironment; } /// /// Providers can override this setting, default is 10 /// public virtual int DefaultMinPasswordLength { get { return 10; } } /// /// Providers can override this setting, default is 0 /// public virtual int DefaultMinNonAlphanumericChars { get { return 0; } } /// /// Providers can override this setting, default is false to use better security /// public virtual bool DefaultUseLegacyEncoding { get { return false; } } /// /// Returns the raw password value for a given user /// /// /// /// /// By default this will return an invalid attempt, inheritors will need to override this to support it /// protected virtual Attempt GetRawPassword(string username) { return Attempt.Fail(); } 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 _requiresUniqueEmail; public bool UseLegacyEncoding { get; private set; } #region Properties public string CustomHashAlgorithmType { get; private set; } /// /// 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; } } /// /// Always returns false, question/answer is not supported /// public override bool RequiresQuestionAndAnswer => false; /// /// 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", true); _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(_hostingEnvironment); //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"); Current.Logger.LogError(ex, "Cannot specify a Hashed password format with the enabledPasswordRetrieval option set to true"); 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. /// Required to change a user password if the user is not new and AllowManuallyChangingPassword is false /// The new password for the specified user. /// /// true if the password was updated successfully; otherwise, false. /// public override bool ChangePassword(string username, string oldPassword, string newPassword) { string rawPasswordValue = string.Empty; 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."); } //Special cases to allow changing password without validating existing credentials // * the member is new and doesn't have a password set // * during installation to set the admin password var installing = Current.RuntimeState.Level == RuntimeLevel.Install; if (rawPasswordValue.StartsWith(Constants.Security.EmptyPasswordPrefix) || (installing && oldPassword == "default")) { return PerformChangePassword(username, oldPassword, newPassword); } if (!oldPassword.IsNullOrWhiteSpace()) { 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 (!password.IsNullOrWhiteSpace()) { 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 retrieval 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 retrieval is enabled /// /// /// /// protected abstract string PerformGetPassword(string username, string answer); public override string ResetPassword(string username, string answer) { 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(IHostingEnvironment hostingEnvironment) { 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; } internal static bool IsEmailValid(string email) { return new EmailAddressAttribute().IsValid(email); } 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 (PasswordFormat) { case MembershipPasswordFormat.Clear: return pass; case MembershipPasswordFormat.Hashed: throw new ProviderException("Provider can not decrypt hashed password"); case MembershipPasswordFormat.Encrypted: default: var bytes = DecryptPassword(Convert.FromBase64String(pass)); return bytes == null ? null : Encoding.Unicode.GetString(bytes, 16, bytes.Length - 16); } } /// /// 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(); } } }