// Copyright © 2004, 2014, Oracle and/or its affiliates. All rights reserved. // // MySQL Connector/NET is licensed under the terms of the GPLv2 // , like most // MySQL Connectors. There are special exceptions to the terms and // conditions of the GPLv2 as it is applied to this software, see the // FLOSS License Exception // . // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published // by the Free Software Foundation; version 2 of the License. // // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License // for more details. // // You should have received a copy of the GNU General Public License along // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA // This code was contributed by Sean Wright (srwright@alcor.concordia.ca) on 2007-01-12 // The copyright was assigned and transferred under the terms of // the MySQL Contributor License Agreement (CLA) using System; using System.Collections.Specialized; using System.Configuration; using System.Configuration.Provider; using System.Data; using System.Diagnostics; using System.Security.Cryptography; using System.Text; using System.Web.Configuration; using System.Web.Hosting; using System.Web.Security; using MySql.Data.MySqlClient; using MySql.Web.Properties; using MySql.Web.Profile; using MySql.Web.Common; using System.Text.RegularExpressions; using MySql.Web.General; namespace MySql.Web.Security { /// /// Manages storage of membership information for an ASP.NET application in a MySQL database. /// /// /// /// This class is used by the and classes /// to provide membership services for ASP.NET applications using a MySQL database. /// /// /// /// /// public sealed class MySQLMembershipProvider : MembershipProvider { private int newPasswordLength = 8; private string eventSource = "MySQLMembershipProvider"; private string eventLog = "Application"; private string exceptionMessage = "An exception occurred. Please check the Event Log."; private string connectionString; private int minRequiredPasswordLength; private bool writeExceptionsToEventLog; private bool enablePasswordReset; private bool enablePasswordRetrieval; private bool requiresQuestionAndAnswer; private bool requiresUniqueEmail; private int maxInvalidPasswordAttempts; private int passwordAttemptWindow; private MembershipPasswordFormat passwordFormat; private int minRequiredNonAlphanumericCharacters; private string passwordStrengthRegularExpression; private Application app; /// /// Initializes the MySQL membership provider with the property values specified in the /// ASP.NET application's configuration file. This method is not intended to be used directly /// from your code. /// /// The name of the instance to initialize. /// A collection of the name/value pairs representing the /// provider-specific attributes specified in the configuration for this provider. /// config is a null reference. /// An attempt is made to call on a provider after the provider has already been initialized. /// public override void Initialize(string name, NameValueCollection config) { if (config == null) { throw new ArgumentNullException("config"); } if (name == null || name.Length == 0) { name = "MySQLMembershipProvider"; } if (string.IsNullOrEmpty(config["description"])) { config.Remove("description"); config.Add("description", "MySQL default application"); } base.Initialize(name, config); string applicationName = GetConfigValue(config["applicationName"], HostingEnvironment.ApplicationVirtualPath); maxInvalidPasswordAttempts = Convert.ToInt32(GetConfigValue(config["maxInvalidPasswordAttempts"], "5")); passwordAttemptWindow = Convert.ToInt32(GetConfigValue(config["passwordAttemptWindow"], "10")); minRequiredNonAlphanumericCharacters = Convert.ToInt32(GetConfigValue(config["minRequiredNonalphanumericCharacters"], "1")); minRequiredPasswordLength = Convert.ToInt32(GetConfigValue(config["minRequiredPasswordLength"], "7")); passwordStrengthRegularExpression = Convert.ToString(GetConfigValue(config["passwordStrengthRegularExpression"], "")); enablePasswordReset = Convert.ToBoolean(GetConfigValue(config["enablePasswordReset"], "True")); enablePasswordRetrieval = Convert.ToBoolean( GetConfigValue(config["enablePasswordRetrieval"], "False")); requiresQuestionAndAnswer = Convert.ToBoolean(GetConfigValue(config["requiresQuestionAndAnswer"], "False")); requiresUniqueEmail = Convert.ToBoolean(GetConfigValue(config["requiresUniqueEmail"], "True")); writeExceptionsToEventLog = Convert.ToBoolean(GetConfigValue(config["writeExceptionsToEventLog"], "True")); string temp_format = config["passwordFormat"]; if (temp_format == null) temp_format = "hashed"; else temp_format = temp_format.ToLowerInvariant(); if (temp_format == "hashed") passwordFormat = MembershipPasswordFormat.Hashed; else if (temp_format == "encrypted") passwordFormat = MembershipPasswordFormat.Encrypted; else if (temp_format == "clear") passwordFormat = MembershipPasswordFormat.Clear; else throw new ProviderException("Password format not supported."); // if the user is asking for the ability to retrieve hashed passwords, then let // them know we can't if (PasswordFormat == MembershipPasswordFormat.Hashed) { if (EnablePasswordRetrieval) throw new ProviderException(Resources.CannotRetrieveHashedPasswords); } ConnectionStringSettings ConnectionStringSettings = ConfigurationManager.ConnectionStrings[ config["connectionStringName"]]; if (ConnectionStringSettings != null) connectionString = ConnectionStringSettings.ConnectionString.Trim(); else connectionString = ""; if (String.IsNullOrEmpty(connectionString)) return; // make sure we have the correct schema SchemaManager.CheckSchema(connectionString, config); app = new Application(applicationName, base.Description); } private static string GetConfigValue(string configValue, string defaultValue) { if (string.IsNullOrEmpty(configValue)) { return defaultValue; } return configValue; } #region Properties /// /// The name of the application using the MySQL membership provider. /// /// The name of the application using the MySQL membership provider. The default is the /// application virtual path. /// The ApplicationName is used by the MySqlMembershipProvider to separate /// membership information for multiple applications. Using different application names, /// applications can use the same membership database. /// Likewise, multiple applications can make use of the same membership data by simply using /// the same application name. /// Caution should be taken with multiple applications as the ApplicationName property is not /// thread safe during writes. /// /// /// The following example shows the membership element being used in an applications web.config file. /// The application name setting is being used. /// /// public override string ApplicationName { get { return app.Name; } set { lock (this) { if (value.ToLowerInvariant() == app.Name.ToLowerInvariant()) return; app = new Application(value, String.Empty); } } } /// /// 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. /// Allows the user to replace their password with a new, randomly generated password. /// This can be especially handy when using hashed passwords since hashed passwords cannot be /// retrieved. /// /// The following example shows the membership element being used in an applications web.config file. /// /// 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. /// If the system is configured to use hashed passwords, then retrieval is not possible. /// If the user attempts to initialize the provider with hashed passwords and enable password retrieval /// set to true then a is thrown. /// /// The following example shows the membership element being used in an applications web.config file. /// /// public override bool EnablePasswordRetrieval { get { return enablePasswordRetrieval; } } /// /// 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 false. /// /// The following example shows the membership element being used in an applications web.config file. /// /// 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. /// /// The following example shows the membership element being used in an applications web.config file. /// /// public override bool RequiresUniqueEmail { get { return requiresUniqueEmail; } } /// /// 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. /// /// The following example shows the membership element being used in an applications web.config file. /// /// public override int MaxInvalidPasswordAttempts { get { return maxInvalidPasswordAttempts; } } /// /// 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. /// /// The following example shows the membership element being used in an applications web.config file. /// /// 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. /// /// The following example shows the membership element being used in an applications web.config file. /// /// public override MembershipPasswordFormat PasswordFormat { get { return passwordFormat; } } /// /// 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. /// /// The following example shows the membership element being used in an applications web.config file. /// /// public override int MinRequiredNonAlphanumericCharacters { get { return minRequiredNonAlphanumericCharacters; } } /// /// Gets the minimum length required for a password. /// /// The minimum length required for a password. /// /// The following example shows the membership element being used in an applications web.config file. /// /// public override int MinRequiredPasswordLength { get { return minRequiredPasswordLength; } } /// /// Gets the regular expression used to evaluate a password. /// /// A regular expression used to evaluate a password. /// /// The following example shows the membership element being used in an applications web.config file. /// In this example, the regular expression specifies that the password must meet the following /// criteria: ///
    /// Is at least seven characters. /// Contains at least one digit. /// Contains at least one special (non-alphanumeric) character. ///
/// ///
public override string PasswordStrengthRegularExpression { get { return passwordStrengthRegularExpression; } } /// /// Gets or sets a value indicating whether exceptions are written to the event log. /// /// /// true if exceptions should be written to the log; otherwise, false. /// public bool WriteExceptionsToEventLog { get { return writeExceptionsToEventLog; } set { writeExceptionsToEventLog = value; } } #endregion #region Public Methods /// /// Changes the password. /// /// The username. /// The old password. /// The new password. /// true if the password was updated successfully, false if the supplied old password /// is invalid, the user is locked out, or the user does not exist in the database. public override bool ChangePassword(string username, string oldPassword, string newPassword) { // this will return false if the username doesn't exist if (!(ValidateUser(username, oldPassword))) return false; ValidatePasswordEventArgs args = new ValidatePasswordEventArgs(username, newPassword, true); OnValidatingPassword(args); if (args.Cancel) { if (!(args.FailureInformation == null)) throw args.FailureInformation; else throw new ProviderException(Resources.ChangePasswordCanceled); } // validate the password according to current guidelines if (!ValidatePassword(newPassword, "newPassword", true)) return false; try { using (MySqlConnection connection = new MySqlConnection(connectionString)) { connection.Open(); // retrieve the existing key and format for this user string passwordKey; MembershipPasswordFormat passwordFormat; int userId = GetUserId(connection, username); GetPasswordInfo(connection, userId, out passwordKey, out passwordFormat); MySqlCommand cmd = new MySqlCommand( @"UPDATE my_aspnet_membership SET Password = @pass, LastPasswordChangedDate = @lastPasswordChangedDate WHERE userId=@userId", connection); cmd.Parameters.AddWithValue("@pass", EncodePassword(newPassword, passwordKey, passwordFormat)); cmd.Parameters.AddWithValue("@lastPasswordChangedDate", DateTime.Now); cmd.Parameters.AddWithValue("@userId", userId); return cmd.ExecuteNonQuery() > 0; } } catch (MySqlException e) { if (WriteExceptionsToEventLog) WriteToEventLog(e, "ChangePassword"); throw new ProviderException(exceptionMessage, e); } } /// /// Changes the password question and answer. /// /// The username. /// The password. /// The new password question. /// The new password answer. /// true if the update was successful; otherwise, false. A value of false is /// also returned if the password is incorrect, the user is locked out, or the user /// does not exist in the database. public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPwdQuestion, string newPwdAnswer) { // this handles the case where the username doesn't exist if (!(ValidateUser(username, password))) return false; try { using (MySqlConnection connection = new MySqlConnection(connectionString)) { connection.Open(); string passwordKey; MembershipPasswordFormat passwordFormat; int userId = GetUserId(connection, username); GetPasswordInfo(connection, userId, out passwordKey, out passwordFormat); MySqlCommand cmd = new MySqlCommand( @"UPDATE my_aspnet_membership SET PasswordQuestion = @passwordQuestion, PasswordAnswer = @passwordAnswer WHERE userId=@userId", connection); cmd.Parameters.AddWithValue("@passwordQuestion", newPwdQuestion); cmd.Parameters.AddWithValue("@passwordAnswer", EncodePassword(newPwdAnswer, passwordKey, passwordFormat)); cmd.Parameters.AddWithValue("@userId", userId); return cmd.ExecuteNonQuery() > 0; } } catch (MySqlException e) { if (WriteExceptionsToEventLog) WriteToEventLog(e, "ChangePasswordQuestionAndAnswer"); throw new ProviderException(exceptionMessage, e); } } /// /// 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. /// public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status) { //basis on MSDN documentation we should trim all the paramater values: http://msdn.microsoft.com/en-us/library/d8t4h2es%28v=vs.110%29.aspx TrimParametersValues(ref username, ref password, ref email, ref passwordQuestion, ref passwordAnswer); ValidatePasswordEventArgs Args = new ValidatePasswordEventArgs(username, password, true); OnValidatingPassword(Args); if (Args.Cancel) { status = MembershipCreateStatus.InvalidPassword; return null; } if (RequiresUniqueEmail && !String.IsNullOrEmpty(GetUserNameByEmail(email))) { status = MembershipCreateStatus.DuplicateEmail; return null; } ValidateQA(passwordQuestion, passwordAnswer); // now try to validate the password if (!ValidatePassword(password, "password", false)) { status = MembershipCreateStatus.InvalidPassword; return null; } // now check to see if we already have a member by this name MembershipUser u = GetUser(username, false); if (u != null) { status = MembershipCreateStatus.DuplicateUserName; return null; } string passwordKey = GetPasswordKey(); DateTime createDate = DateTime.Now; MySqlTransaction transaction = null; using (MySqlConnection connection = new MySqlConnection(connectionString)) { try { connection.Open(); transaction = connection.BeginTransaction(); // either create a new user or fetch the existing user id long userId = SchemaManager.CreateOrFetchUserId(connection, username, app.EnsureId(connection), true); MySqlCommand cmd = new MySqlCommand( @"INSERT INTO my_aspnet_membership VALUES(@userId, @email, @comment, @password, @passwordKey, @passwordFormat, @passwordQuestion, @passwordAnswer, @isApproved, @lastActivityDate, @lastLoginDate, @lastPasswordChangedDate, @creationDate, @isLockedOut, @lastLockedOutDate, @failedPasswordAttemptCount, @failedPasswordAttemptWindowStart, @failedPasswordAnswerAttemptCount, @failedPasswordAnswerAttemptWindowStart)", connection); cmd.Parameters.AddWithValue("@userId", userId); cmd.Parameters.AddWithValue("@email", email); cmd.Parameters.AddWithValue("@comment", ""); cmd.Parameters.AddWithValue("@password", EncodePassword(password, passwordKey, PasswordFormat)); cmd.Parameters.AddWithValue("@passwordKey", passwordKey); cmd.Parameters.AddWithValue("@passwordFormat", PasswordFormat); cmd.Parameters.AddWithValue("@passwordQuestion", passwordQuestion); cmd.Parameters.AddWithValue("@passwordAnswer", EncodePassword(passwordAnswer, passwordKey, PasswordFormat)); cmd.Parameters.AddWithValue("@isApproved", isApproved); cmd.Parameters.AddWithValue("@lastActivityDate", createDate); cmd.Parameters.AddWithValue("@lastLoginDate", createDate); cmd.Parameters.AddWithValue("@lastPasswordChangedDate", createDate); cmd.Parameters.AddWithValue("@creationDate", createDate); cmd.Parameters.AddWithValue("@isLockedOut", false); cmd.Parameters.AddWithValue("@lastLockedOutDate", createDate); cmd.Parameters.AddWithValue("@failedPasswordAttemptCount", 0); cmd.Parameters.AddWithValue("@failedPasswordAttemptWindowStart", createDate); cmd.Parameters.AddWithValue("@failedPasswordAnswerAttemptCount", 0); cmd.Parameters.AddWithValue("@failedPasswordAnswerAttemptWindowStart", createDate); int recAdded = cmd.ExecuteNonQuery(); if (recAdded > 0) status = MembershipCreateStatus.Success; else status = MembershipCreateStatus.UserRejected; transaction.Commit(); } catch (MySqlException e) { if (WriteExceptionsToEventLog) WriteToEventLog(e, "CreateUser"); status = MembershipCreateStatus.ProviderError; if (transaction != null) transaction.Rollback(); return null; } } return GetUser(username, false); } /// /// Removes a user from the membership data source. /// /// The name of the user to delete. /// true to delete data related to the user from the database; false to leave data related to the user in the database. /// /// true if the user was successfully deleted; otherwise, false. /// public override bool DeleteUser(string username, bool deleteAllRelatedData) { try { using (MySqlConnection conn = new MySqlConnection(connectionString)) { conn.Open(); int userId = GetUserId(conn, username); if (-1 == userId) return false; // if we are supposed to delete all related data, then delegate that to those providers if (deleteAllRelatedData) { MySQLRoleProvider.DeleteUserData(conn, userId); MySQLProfileProvider.DeleteUserData(conn, userId); } string sql = @"DELETE {0}m FROM my_aspnet_users u, my_aspnet_membership m WHERE u.id=m.userId AND u.id=@userId"; MySqlCommand cmd = new MySqlCommand( String.Format(sql, deleteAllRelatedData ? "u," : ""), conn); cmd.Parameters.AddWithValue("@appId", app.FetchId(conn)); cmd.Parameters.AddWithValue("@userId", userId); return cmd.ExecuteNonQuery() > 0; } } catch (MySqlException e) { if (WriteExceptionsToEventLog) WriteToEventLog(e, "DeleteUser"); throw new ProviderException(exceptionMessage, e); } } /// /// Gets a collection of all the users in the data source in pages of data. /// /// The index of the page of results to return. is zero-based. /// The size of the page of results to return. /// The total number of matched users. /// /// A collection that contains a page of objects beginning at the page specified by . /// public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords) { return GetUsers(null, null, pageIndex, pageSize, out totalRecords); } /// /// Gets the number of users currently accessing the application. /// /// /// The number of users currently accessing the application. /// public override int GetNumberOfUsersOnline() { TimeSpan onlineSpan = new TimeSpan(0, Membership.UserIsOnlineTimeWindow, 0); DateTime compareTime = DateTime.Now.Subtract(onlineSpan); try { using (MySqlConnection connection = new MySqlConnection(connectionString)) { connection.Open(); MySqlCommand cmd = new MySqlCommand( @"SELECT COUNT(*) FROM my_aspnet_membership m JOIN my_aspnet_users u ON m.userId=u.id WHERE m.LastActivityDate > @date AND u.applicationId=@appId", connection); cmd.Parameters.AddWithValue("@date", compareTime); cmd.Parameters.AddWithValue("@appId", app.FetchId(connection)); return Convert.ToInt32(cmd.ExecuteScalar()); } } catch (MySqlException e) { if (WriteExceptionsToEventLog) WriteToEventLog(e, "GetNumberOfUsersOnline"); throw new ProviderException(exceptionMessage, e); } } /// /// Gets the password for the specified user name from the data source. /// /// The user to retrieve the password for. /// The password answer for the user. /// /// The password for the specified user name. /// public override string GetPassword(string username, string answer) { if (!EnablePasswordRetrieval) throw new ProviderException(Resources.PasswordRetrievalNotEnabled); try { using (MySqlConnection connection = new MySqlConnection(connectionString)) { connection.Open(); int userId = GetUserId(connection, username); if (-1 == userId) throw new ProviderException("Username not found."); string sql = @"SELECT Password, PasswordAnswer, PasswordKey, PasswordFormat, IsLockedOut FROM my_aspnet_membership WHERE userId=@userId"; MySqlCommand cmd = new MySqlCommand(sql, connection); cmd.Parameters.AddWithValue("@userId", userId); using (MySqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SingleRow)) { reader.Read(); if (reader.GetBoolean("IsLockedOut")) throw new MembershipPasswordException(Resources.UserIsLockedOut); string password = reader.GetString("Password"); string passwordAnswer = reader.GetValue(reader.GetOrdinal("PasswordAnswer")).ToString(); string passwordKey = reader.GetString("PasswordKey"); MembershipPasswordFormat format = (MembershipPasswordFormat)reader.GetInt32(3); reader.Close(); if (RequiresQuestionAndAnswer && !(CheckPassword(answer, passwordAnswer, passwordKey, format))) { UpdateFailureCount(userId, "PasswordAnswer", connection); throw new MembershipPasswordException(Resources.IncorrectPasswordAnswer); } if (PasswordFormat == MembershipPasswordFormat.Encrypted) { password = UnEncodePassword(password, format); } return password; } } } catch (MySqlException e) { if (WriteExceptionsToEventLog) WriteToEventLog(e, "GetPassword"); throw new ProviderException(exceptionMessage, e); } } /// /// Gets information from the data source for a user. Provides an option to update the last-activity date/time stamp for the user. /// /// The name of the user to get information for. /// true to update the last-activity date/time stamp for the user; false to return user information without updating the last-activity date/time stamp for the user. /// /// A object populated with the specified user's information from the data source. /// public override MembershipUser GetUser(string username, bool userIsOnline) { try { int userId = -1; using (MySqlConnection connection = new MySqlConnection(connectionString)) { connection.Open(); userId = GetUserId(connection, username); if (-1 == userId) return null; } return GetUser(userId, userIsOnline); } catch (MySqlException e) { if (WriteExceptionsToEventLog) WriteToEventLog(e, "GetUser(String, Boolean)"); throw new ProviderException(exceptionMessage, e); } } /// /// Gets user information from the data source based on the unique identifier for the membership user. Provides an option to update the last-activity date/time stamp for the user. /// /// The unique identifier for the membership user to get information for. /// true to update the last-activity date/time stamp for the user; false to return user information without updating the last-activity date/time stamp for the user. /// /// A object populated with the specified user's information from the data source. /// public override MembershipUser GetUser(object providerUserKey, bool userIsOnline) { MySqlTransaction txn = null; try { using (MySqlConnection connection = new MySqlConnection(connectionString)) { connection.Open(); txn = connection.BeginTransaction(); MySqlCommand cmd = new MySqlCommand("", connection); cmd.Parameters.AddWithValue("@userId", providerUserKey); if (userIsOnline) { cmd.CommandText = @"UPDATE my_aspnet_users SET lastActivityDate = @date WHERE id=@userId"; cmd.Parameters.AddWithValue("@date", DateTime.Now); cmd.ExecuteNonQuery(); cmd.CommandText = "UPDATE my_aspnet_membership SET LastActivityDate=@date WHERE userId=@userId"; cmd.ExecuteNonQuery(); } cmd.CommandText = @"SELECT m.*,u.name FROM my_aspnet_membership m JOIN my_aspnet_users u ON m.userId=u.id WHERE u.id=@userId"; MembershipUser user; using (MySqlDataReader reader = cmd.ExecuteReader()) { if (!reader.Read()) return null; user = GetUserFromReader(reader); } txn.Commit(); return user; } } catch (MySqlException e) { if (txn != null) txn.Rollback(); if (WriteExceptionsToEventLog) WriteToEventLog(e, "GetUser(Object, Boolean)"); throw new ProviderException(exceptionMessage); } } /// /// Unlocks the user. /// /// The username. /// true if the membership user was successfully unlocked; /// otherwise, false. A value of false is also returned if the user /// does not exist in the database. public override bool UnlockUser(string username) { try { using (MySqlConnection conn = new MySqlConnection(connectionString)) { conn.Open(); int userId = GetUserId(conn, username); if (-1 == userId) return false; string sql = @"UPDATE my_aspnet_membership SET IsLockedOut = false, LastLockedOutDate = @lastDate WHERE userId=@userId"; MySqlCommand cmd = new MySqlCommand(sql, conn); cmd.Parameters.AddWithValue("@lastDate", DateTime.Now); cmd.Parameters.AddWithValue("@userId", userId); return cmd.ExecuteNonQuery() > 0; } } catch (MySqlException e) { if (WriteExceptionsToEventLog) WriteToEventLog(e, "UnlockUser"); throw new ProviderException(exceptionMessage, e); } } /// /// Gets the user name associated with the specified e-mail address. /// /// The e-mail address to search for. /// /// The user name associated with the specified e-mail address. If no match is found, return null. /// public override string GetUserNameByEmail(string email) { try { using (MySqlConnection conn = new MySqlConnection(connectionString)) { conn.Open(); string sql = @"SELECT u.name FROM my_aspnet_users u JOIN my_aspnet_membership m ON m.userid=u.id WHERE m.Email = @email AND u.applicationId=@appId"; MySqlCommand cmd = new MySqlCommand(sql, conn); cmd.Parameters.AddWithValue("@email", email); cmd.Parameters.AddWithValue("@appId", app.FetchId(conn)); return (string)cmd.ExecuteScalar(); } } catch (MySqlException e) { if (WriteExceptionsToEventLog) WriteToEventLog(e, "GetUserNameByEmail"); throw new ProviderException(exceptionMessage); } } /// /// Resets a user's password to a new, automatically generated password. /// /// The user to reset the password for. /// The password answer for the specified user. /// The new password for the specified user. public override string ResetPassword(string username, string answer) { if (!(EnablePasswordReset)) throw new NotSupportedException(Resources.PasswordResetNotEnabled); try { using (MySqlConnection connection = new MySqlConnection(connectionString)) { connection.Open(); // fetch the userid first int userId = GetUserId(connection, username); if (-1 == userId) throw new ProviderException(Resources.UsernameNotFound); if (answer == null && RequiresQuestionAndAnswer) { UpdateFailureCount(userId, "PasswordAnswer", connection); throw new ProviderException(Resources.PasswordRequiredForReset); } string newPassword = Membership.GeneratePassword(newPasswordLength, MinRequiredNonAlphanumericCharacters); ValidatePasswordEventArgs Args = new ValidatePasswordEventArgs(username, newPassword, true); OnValidatingPassword(Args); if (Args.Cancel) { if (!(Args.FailureInformation == null)) throw Args.FailureInformation; else throw new MembershipPasswordException(Resources.PasswordResetCanceledNotValid); } MySqlCommand cmd = new MySqlCommand(@"SELECT PasswordAnswer, PasswordKey, PasswordFormat, IsLockedOut FROM my_aspnet_membership WHERE userId=@userId", connection); cmd.Parameters.AddWithValue("@userId", userId); string passwordKey = String.Empty; MembershipPasswordFormat format; using (MySqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SingleRow)) { reader.Read(); if (reader.GetBoolean("IsLockedOut")) throw new MembershipPasswordException(Resources.UserIsLockedOut); object passwordAnswer = reader.GetValue(reader.GetOrdinal("PasswordAnswer")); passwordKey = reader.GetString("PasswordKey"); format = (MembershipPasswordFormat)reader.GetByte("PasswordFormat"); reader.Close(); if (RequiresQuestionAndAnswer) { if (!CheckPassword(answer, (string)passwordAnswer, passwordKey, format)) { UpdateFailureCount(userId, "PasswordAnswer", connection); throw new MembershipPasswordException(Resources.IncorrectPasswordAnswer); } } } cmd.CommandText = @"UPDATE my_aspnet_membership SET Password = @pass, LastPasswordChangedDate = @lastPassChange WHERE userId=@userId"; cmd.Parameters.AddWithValue("@pass", EncodePassword(newPassword, passwordKey, format)); cmd.Parameters.AddWithValue("@lastPassChange", DateTime.Now); int rowsAffected = cmd.ExecuteNonQuery(); if (rowsAffected != 1) throw new MembershipPasswordException(Resources.ErrorResettingPassword); return newPassword; } } catch (MySqlException e) { if (WriteExceptionsToEventLog) WriteToEventLog(e, "ResetPassword"); throw new ProviderException(exceptionMessage, e); } } /// /// Updates information about a user in the data source. /// /// A object /// that represents the user to update and the updated information for the user. public override void UpdateUser(MembershipUser user) { try { using (MySqlConnection conn = new MySqlConnection(connectionString)) { conn.Open(); int userId = GetUserId(conn, user.UserName); if (-1 == userId) throw new ProviderException(Resources.UsernameNotFound); string sql = @"UPDATE my_aspnet_membership m, my_aspnet_users u SET m.Email=@email, m.Comment=@comment, m.IsApproved=@isApproved, m.LastLoginDate=@lastLoginDate, u.lastActivityDate=@lastActivityDate, m.LastActivityDate=@lastActivityDate WHERE m.userId=u.id AND u.name LIKE @name AND u.applicationId=@appId"; MySqlCommand cmd = new MySqlCommand(sql, conn); cmd.Parameters.AddWithValue("@Email", user.Email); cmd.Parameters.AddWithValue("@Comment", user.Comment); cmd.Parameters.AddWithValue("@isApproved", user.IsApproved); cmd.Parameters.AddWithValue("@lastLoginDate", user.LastLoginDate); cmd.Parameters.AddWithValue("@lastActivityDate", user.LastActivityDate); cmd.Parameters.AddWithValue("@name", user.UserName); cmd.Parameters.AddWithValue("@appId", app.FetchId(conn)); cmd.ExecuteNonQuery(); } } catch (MySqlException e) { if (WriteExceptionsToEventLog) WriteToEventLog(e, "UpdateUser"); throw new ProviderException(exceptionMessage); } } /// /// Verifies that the specified user name and password exist in the data source. /// /// The name of the user to validate. /// The password for the specified user. /// /// true if the specified username and password are valid; otherwise, false. /// public override bool ValidateUser(string username, string password) { bool isValid = false; try { using (MySqlConnection connection = new MySqlConnection(connectionString)) { connection.Open(); // first get the user id. If that is -1, then the user doesn't exist // so we just return false since we can't bump any counters int userId = GetUserId(connection, username); if (-1 == userId) return false; string sql = @"SELECT Password, PasswordKey, PasswordFormat, IsApproved, Islockedout FROM my_aspnet_membership WHERE userId=@userId"; MySqlCommand cmd = new MySqlCommand(sql, connection); cmd.Parameters.AddWithValue("@userId", userId); using (MySqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SingleRow)) { if (!reader.HasRows) return false; reader.Read(); if (reader.GetBoolean("IsLockedOut")) return false; string pwd = reader.GetString(0); string passwordKey = reader.GetString(1); MembershipPasswordFormat format = (MembershipPasswordFormat) reader.GetInt32(2); bool isApproved = reader.GetBoolean(3); reader.Close(); if (!CheckPassword(password, pwd, passwordKey, format)) UpdateFailureCount(userId, "Password", connection); else if (isApproved) { isValid = true; DateTime currentDate = DateTime.Now; MySqlCommand updateCmd = new MySqlCommand( @"UPDATE my_aspnet_membership m, my_aspnet_users u SET m.LastLoginDate = @lastLoginDate, u.lastActivityDate = @date, m.LastActivityDate=@date WHERE m.userId=@userid AND u.id=@userid", connection); updateCmd.Parameters.AddWithValue("@lastLoginDate", currentDate); updateCmd.Parameters.AddWithValue("@date", currentDate); updateCmd.Parameters.AddWithValue("@userid", userId); updateCmd.ExecuteNonQuery(); } } return isValid; } } catch (MySqlException e) { if (WriteExceptionsToEventLog) WriteToEventLog(e, "ValidateUser"); throw new ProviderException(exceptionMessage, e); } } /// /// Gets a collection of membership users where the user name contains the specified user name to match. /// /// The user name to search for. /// The index of the page of results to return. is zero-based. /// The size of the page of results to return. /// The total number of matched users. /// /// A collection that contains a page of objects beginning at the page specified by . /// public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords) { return GetUsers(usernameToMatch, null, pageIndex, pageSize, out totalRecords); } /// /// Gets a collection of membership users where the e-mail address contains the specified e-mail address to match. /// /// The e-mail address to search for. /// The index of the page of results to return. is zero-based. /// The size of the page of results to return. /// The total number of matched users. /// /// A collection that contains a page of objects beginning at the page specified by . /// public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords) { return GetUsers(null, emailToMatch, pageIndex, pageSize, out totalRecords); } #endregion #region Private Methods private int GetUserId(MySqlConnection connection, string username) { MySqlCommand cmd = new MySqlCommand( "SELECT id FROM my_aspnet_users WHERE name = @name AND applicationId=@appId", connection); cmd.Parameters.AddWithValue("@name", username); cmd.Parameters.AddWithValue("@appId", app.FetchId(connection)); object id = cmd.ExecuteScalar(); if (id == null) return -1; return (int)id; } private void WriteToEventLog(Exception e, string action) { using (EventLog log = new EventLog()) { log.Source = eventSource; log.Log = eventLog; string message = "An exception occurred communicating with the data source." + Environment.NewLine + Environment.NewLine; message += "Action: " + action + Environment.NewLine + Environment.NewLine; message += "Exception: " + e; log.WriteEntry(message); } } private MembershipUser GetUserFromReader(MySqlDataReader reader) { object providerUserKey = reader.GetInt32("userId"); string username = reader.GetString("name"); string email = null; if (!reader.IsDBNull(reader.GetOrdinal("Email"))) email = reader.GetString("Email"); string passwordQuestion = ""; if (!(reader.GetValue(reader.GetOrdinal("PasswordQuestion")) == DBNull.Value)) passwordQuestion = reader.GetString("PasswordQuestion"); string comment = ""; if (!(reader.GetValue(reader.GetOrdinal("Comment")) == DBNull.Value)) comment = reader.GetString("Comment"); bool isApproved = reader.GetBoolean("IsApproved"); bool isLockedOut = reader.GetBoolean("IsLockedOut"); DateTime creationDate = reader.GetDateTime("CreationDate"); DateTime lastLoginDate = new DateTime(); if (!(reader.GetValue(reader.GetOrdinal("LastLoginDate")) == DBNull.Value)) lastLoginDate = reader.GetDateTime("LastLoginDate"); DateTime lastActivityDate = reader.GetDateTime("LastActivityDate"); DateTime lastPasswordChangedDate = reader.GetDateTime("LastPasswordChangedDate"); DateTime lastLockedOutDate = new DateTime(); if (!(reader.GetValue(reader.GetOrdinal("LastLockedoutDate")) == DBNull.Value)) lastLockedOutDate = reader.GetDateTime("LastLockedoutDate"); MembershipUser u = new MembershipUser(Name, username, providerUserKey, email, passwordQuestion, comment, isApproved, isLockedOut, creationDate, lastLoginDate, lastActivityDate, lastPasswordChangedDate, lastLockedOutDate); return u; } private string UnEncodePassword(string encodedPassword, MembershipPasswordFormat format) { string password = encodedPassword; if (format == MembershipPasswordFormat.Clear) return encodedPassword; else if (format == MembershipPasswordFormat.Encrypted) return Encoding.Unicode.GetString(DecryptPassword( Convert.FromBase64String(password))); else if (format == MembershipPasswordFormat.Hashed) throw new ProviderException(Resources.CannotUnencodeHashedPwd); else throw new ProviderException(Resources.UnsupportedPasswordFormat); } private string GetPasswordKey() { RNGCryptoServiceProvider cryptoProvider = new RNGCryptoServiceProvider(); byte[] key = new byte[16]; cryptoProvider.GetBytes(key); return Convert.ToBase64String(key); } /// /// this method is only necessary because early versions of Mono did not support /// the HashAlgorithmType property /// /// /// private string HashPasswordBytes(byte[] key, byte[] bytes) { HashAlgorithm hash = HashAlgorithm.Create(Membership.HashAlgorithmType); if (hash is KeyedHashAlgorithm) { KeyedHashAlgorithm keyedHash = hash as KeyedHashAlgorithm; keyedHash.Key = key; } return Convert.ToBase64String(hash.ComputeHash(bytes)); } private string EncodePassword(string password, string passwordKey, MembershipPasswordFormat format) { if (password == null) return null; if (format == MembershipPasswordFormat.Clear) return password; byte[] passwordBytes = Encoding.Unicode.GetBytes(password); byte[] keyBytes = Convert.FromBase64String(passwordKey); byte[] keyedBytes = new byte[passwordBytes.Length + keyBytes.Length]; Array.Copy(keyBytes, keyedBytes, keyBytes.Length); Array.Copy(passwordBytes, 0, keyedBytes, keyBytes.Length, passwordBytes.Length); if (format == MembershipPasswordFormat.Encrypted) { byte[] encryptedBytes = EncryptPassword(passwordBytes); return Convert.ToBase64String(encryptedBytes); } else if (format == MembershipPasswordFormat.Hashed) return HashPasswordBytes(keyBytes, keyedBytes); else throw new ProviderException(Resources.UnsupportedPasswordFormat); } private void UpdateFailureCount(int userId, string failureType, MySqlConnection connection) { MySqlCommand cmd = new MySqlCommand( @"SELECT FailedPasswordAttemptCount, FailedPasswordAttemptWindowStart, FailedPasswordAnswerAttemptCount, FailedPasswordAnswerAttemptWindowStart FROM my_aspnet_membership WHERE userId=@userId", connection); cmd.Parameters.AddWithValue("@userId", userId); DateTime windowStart = new DateTime(); int failureCount = 0; try { using (MySqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SingleRow)) { if (!reader.HasRows) throw new ProviderException(Resources.UnableToUpdateFailureCount); reader.Read(); if (failureType == "Password") { failureCount = reader.GetInt32(0); windowStart = reader.GetDateTime(1); } if (failureType == "PasswordAnswer") { failureCount = reader.GetInt32(2); windowStart = reader.GetDateTime(3); } } DateTime windowEnd = windowStart.AddMinutes(PasswordAttemptWindow); if (failureCount == 0 || DateTime.Now > windowEnd) { if (failureType == "Password") { cmd.CommandText = @"UPDATE my_aspnet_membership SET FailedPasswordAttemptCount = @count, FailedPasswordAttemptWindowStart = @windowStart WHERE userId=@userId"; } if (failureType == "PasswordAnswer") { cmd.CommandText = @"UPDATE my_aspnet_membership SET FailedPasswordAnswerAttemptCount = @count, FailedPasswordAnswerAttemptWindowStart = @windowStart WHERE userId = @userId"; } cmd.Parameters.Clear(); cmd.Parameters.AddWithValue("@count", 1); cmd.Parameters.AddWithValue("@windowStart", DateTime.Now); cmd.Parameters.AddWithValue("@userId", userId); if (cmd.ExecuteNonQuery() < 0) throw new ProviderException(Resources.UnableToUpdateFailureCount); } else { failureCount += 1; if (failureCount >= MaxInvalidPasswordAttempts) { cmd.CommandText = @"UPDATE my_aspnet_membership SET IsLockedOut = @isLockedOut, LastLockedOutDate = @lastLockedOutDate WHERE userId=@userId"; cmd.Parameters.Clear(); cmd.Parameters.AddWithValue("@isLockedOut", true); cmd.Parameters.AddWithValue("@lastLockedOutDate", DateTime.Now); cmd.Parameters.AddWithValue("@userId", userId); if (cmd.ExecuteNonQuery() < 0) throw new ProviderException(Resources.UnableToLockOutUser); } else { if (failureType == "Password") { cmd.CommandText = @"UPDATE my_aspnet_membership SET FailedPasswordAttemptCount = @count WHERE userId=@userId"; } if (failureType == "PasswordAnswer") { cmd.CommandText = @"UPDATE my_aspnet_membership SET FailedPasswordAnswerAttemptCount = @count WHERE userId=@userId"; } cmd.Parameters.Clear(); cmd.Parameters.AddWithValue("@count", failureCount); cmd.Parameters.AddWithValue("@userId", userId); if (cmd.ExecuteNonQuery() < 0) throw new ProviderException("Unable to update failure count."); } } } catch (MySqlException e) { if (WriteExceptionsToEventLog) WriteToEventLog(e, "UpdateFailureCount"); throw new ProviderException(exceptionMessage, e); } } private bool CheckPassword(string password, string dbpassword, string passwordKey, MembershipPasswordFormat format) { password = EncodePassword(password, passwordKey, format); return password == dbpassword; } private void GetPasswordInfo(MySqlConnection connection, int userId, out string passwordKey, out MembershipPasswordFormat passwordFormat) { MySqlCommand cmd = new MySqlCommand( @"SELECT PasswordKey, PasswordFormat FROM my_aspnet_membership WHERE userId=@userId", connection); cmd.Parameters.AddWithValue("@userId", userId); using (MySqlDataReader reader = cmd.ExecuteReader()) { reader.Read(); passwordKey = reader.GetString(reader.GetOrdinal("PasswordKey")); passwordFormat = (MembershipPasswordFormat)reader.GetByte( reader.GetOrdinal("PasswordFormat")); } } private MembershipUserCollection GetUsers(string username, string email, int pageIndex, int pageSize, out int totalRecords) { MembershipUserCollection users = new MembershipUserCollection(); try { using (MySqlConnection connection = new MySqlConnection(connectionString)) { connection.Open(); MySqlCommand cmd = new MySqlCommand(); cmd.Connection = connection; string sql = @"SELECT SQL_CALC_FOUND_ROWS u.name,m.* FROM my_aspnet_users u JOIN my_aspnet_membership m ON m.userId=u.id WHERE u.applicationId=@appId"; if (username != null) { sql += " AND u.name LIKE @name"; cmd.Parameters.AddWithValue("@name", username); } else if (email != null) { sql += " AND m.Email LIKE @email"; cmd.Parameters.AddWithValue("@email", email); } sql += " ORDER BY u.id ASC LIMIT {0},{1}"; cmd.CommandText = String.Format(sql, pageIndex * pageSize, pageSize); cmd.Parameters.AddWithValue("@appId", app.FetchId(connection)); using (MySqlDataReader reader = cmd.ExecuteReader()) { while (reader.Read()) users.Add(GetUserFromReader(reader)); } cmd.CommandText = "SELECT FOUND_ROWS()"; cmd.Parameters.Clear(); totalRecords = Convert.ToInt32(cmd.ExecuteScalar()); } return users; } catch (MySqlException e) { if (WriteExceptionsToEventLog) WriteToEventLog(e, "GetUsers"); throw new ProviderException(exceptionMessage); } } private void ValidateQA(string question, string answer) { if (RequiresQuestionAndAnswer && String.IsNullOrEmpty(question)) throw new ArgumentException(Resources.PasswordQuestionInvalid); if (RequiresQuestionAndAnswer && String.IsNullOrEmpty(answer)) throw new ArgumentException(Resources.PasswordAnswerInvalid); } private bool ValidatePassword(string password, string argumentName, bool throwExceptions) { string exceptionString = null; object correctValue = MinRequiredPasswordLength; if (password.Length < MinRequiredPasswordLength) exceptionString = Resources.PasswordNotLongEnough; else { int count = 0; foreach (char c in password) if (!char.IsLetterOrDigit(c)) count++; if (count < MinRequiredNonAlphanumericCharacters) exceptionString = Resources.NotEnoughNonAlphaNumericInPwd; correctValue = MinRequiredNonAlphanumericCharacters; } if (exceptionString != null) { if (throwExceptions) throw new ArgumentException( string.Format(exceptionString, argumentName, correctValue), argumentName); else return false; } if (PasswordStrengthRegularExpression.Length > 0) if (!Regex.IsMatch(password, PasswordStrengthRegularExpression)) return false; return true; } private void TrimParametersValues(ref string username, ref string password, ref string email, ref string passwordQuestion, ref string passwordAnswer) { username = string.IsNullOrEmpty(username) ? username : username.Trim(); password = string.IsNullOrEmpty(password) ? password : password.Trim(); email = string.IsNullOrEmpty(email) ? email : email.Trim(); passwordQuestion = string.IsNullOrEmpty(passwordQuestion) ? passwordQuestion : passwordQuestion.Trim(); passwordAnswer = string.IsNullOrEmpty(passwordAnswer) ? passwordAnswer : passwordAnswer.Trim(); } #endregion } }