* roottypes.cs: Rename from tree.cs.
[mono.git] / mcs / class / System.Web / System.Web.Security / SqlMembershipProvider.cs
index 44bfd9ac816b033f98ecd7f238f328b328aca1f4..ff16d2d968eeaf61d901f90cdbd8f4820b7bfd49 100644 (file)
@@ -44,6 +44,8 @@ namespace System.Web.Security {
        public class SqlMembershipProvider : MembershipProvider {
 
                const int SALT_BYTES = 16;
+
+               /* can this be done just by setting the datetime fields to 0? */
                DateTime DefaultDateTime = new DateTime (1754,1,1).ToUniversalTime();
 
                bool enablePasswordReset;
@@ -64,8 +66,6 @@ namespace System.Web.Security {
 
                string applicationName;
                
-               byte[] init_vector;
-
                static object lockobj = new object();
 
                void InitConnection ()
@@ -101,30 +101,12 @@ namespace System.Web.Security {
                                throw new ArgumentException (String.Format ("invalid format for {0}", pName));
                }
 
-               string HashAndBase64Encode (string s, byte[] salt)
-               {
-                       byte[] tmp = Encoding.UTF8.GetBytes (s);
-
-                       byte[] hashedBytes = new byte[salt.Length + tmp.Length];
-                       Array.Copy (salt, hashedBytes, salt.Length);
-                       Array.Copy (tmp, 0, hashedBytes, salt.Length, tmp.Length);
-
-                       MembershipSection section = (MembershipSection)WebConfigurationManager.GetSection ("system.web/membership");
-                       string alg_type = section.HashAlgorithmType;
-                       if (alg_type == "")
-                               alg_type = "SHA1";
-                       HashAlgorithm alg = HashAlgorithm.Create (alg_type);
-                       hashedBytes = alg.ComputeHash (hashedBytes);
-
-                       return Convert.ToBase64String (hashedBytes);
-               }
-               
-               string EncryptAndBase64Encode (string s)
+               SymmetricAlgorithm GetAlg (out byte[] decryptionKey)
                {
                        MachineKeySection section = (MachineKeySection)WebConfigurationManager.GetSection ("system.web/machineKey");
 
                        if (section.DecryptionKey.StartsWith ("AutoGenerate"))
-                               throw new Exception ("You must explicitly specify a decryption key in the <machineKey> section when using encrypted passwords.");
+                               throw new ProviderException ("You must explicitly specify a decryption key in the <machineKey> section when using encrypted passwords.");
 
                        string alg_type = section.Decryption;
                        if (alg_type == "Auto")
@@ -136,26 +118,194 @@ namespace System.Web.Security {
                        else if (alg_type == "3DES")
                                alg = TripleDES.Create ();
                        else
-                               throw new Exception (String.Format ("Unsupported decryption attribute '{0}' in <machineKey> configuration section", alg_type));
+                               throw new ProviderException (String.Format ("Unsupported decryption attribute '{0}' in <machineKey> configuration section", alg_type));
+
+                       decryptionKey = section.DecryptionKey192Bits;
+                       return alg;
+               }
 
-                       ICryptoTransform encryptor = alg.CreateEncryptor (section.DecryptionKey192Bits, init_vector);
+                protected override byte[] DecryptPassword (byte[] encodedPassword)
+                {
+                       byte[] decryptionKey;
 
-                       byte[] result = Encoding.UTF8.GetBytes (s);
-                       result = encryptor.TransformFinalBlock (result, 0, result.Length);
+                       using (SymmetricAlgorithm alg = GetAlg (out decryptionKey)) {
+                               alg.Key = decryptionKey;
 
-                       return Convert.ToBase64String (result);
+                               using (ICryptoTransform decryptor = alg.CreateDecryptor ()) {
+
+                                       byte[] buf = decryptor.TransformFinalBlock (encodedPassword, 0, encodedPassword.Length);
+                                       byte[] rv = new byte[buf.Length - SALT_BYTES];
+
+                                       Array.Copy (buf, 16, rv, 0, buf.Length - 16);
+                                       return rv;
+                               }
+                       }
+                }
+
+                protected override byte[] EncryptPassword (byte[] password)
+                {
+                       byte[] decryptionKey;
+                       byte[] iv = new byte[SALT_BYTES];
+
+                       Array.Copy (password, 0, iv, 0, SALT_BYTES);
+                       Array.Clear (password, 0, SALT_BYTES);
+
+                       using (SymmetricAlgorithm alg = GetAlg (out decryptionKey)) {
+                               using (ICryptoTransform encryptor = alg.CreateEncryptor (decryptionKey, iv)) {
+                                       return encryptor.TransformFinalBlock (password, 0, password.Length);
+                               }
+                       }
                }
 
-               [MonoTODO]
                public override bool ChangePassword (string username, string oldPwd, string newPwd)
                {
-                       throw new NotImplementedException ();
+                       if (username != null) username = username.Trim ();
+                       if (oldPwd != null) oldPwd = oldPwd.Trim ();
+                       if (newPwd != null) newPwd = newPwd.Trim ();
+
+                       CheckParam ("username", username, 256);
+                       CheckParam ("oldPwd", oldPwd, 128);
+                       CheckParam ("newPwd", newPwd, 128);
+
+                       MembershipUser user = GetUser (username, false);
+                       if (user == null) throw new ProviderException ("could not find user in membership database");
+                       if (user.IsLockedOut) throw new MembershipPasswordException ("user is currently locked out");
+
+                       InitConnection();
+
+                       DbTransaction trans = connection.BeginTransaction ();
+
+                       string commandText;
+                       DbCommand command;
+
+                       try {
+                               MembershipPasswordFormat passwordFormat;
+                               string db_salt;
+
+                               bool valid = ValidateUsingPassword (trans, username, oldPwd, out passwordFormat, out db_salt);
+                               if (valid) {
+
+                                       EmitValidatingPassword (username, newPwd, false);
+
+                                       string db_password = EncodePassword (newPwd, passwordFormat, db_salt);
+
+                                       DateTime now = DateTime.Now.ToUniversalTime ();
+
+                                       commandText = @"
+UPDATE m
+   SET Password = @Password,
+       FailedPasswordAttemptCount = 0,
+       FailedPasswordAttemptWindowStart = @DefaultDateTime,
+       LastPasswordChangedDate = @Now
+  FROM dbo.aspnet_Membership m, dbo.aspnet_Users u, dbo.aspnet_Applications a
+ WHERE m.ApplicationId = a.ApplicationId
+   AND u.ApplicationId = a.ApplicationId
+   AND m.UserId = u.UserId
+   AND u.LoweredUserName = LOWER(@UserName)
+   AND a.LoweredApplicationName = LOWER(@ApplicationName)";
+
+                                       command = factory.CreateCommand ();
+                                       command.Transaction = trans;
+                                       command.CommandText = commandText;
+                                       command.Connection = connection;
+                                       command.CommandType = CommandType.Text;
+                                       AddParameter (command, "UserName", user.UserName);
+                                       AddParameter (command, "Now", now.ToString ());
+                                       AddParameter (command, "Password", db_password);
+                                       AddParameter (command, "ApplicationName", ApplicationName);
+                                       AddParameter (command, "DefaultDateTime", DefaultDateTime.ToString());
+
+                                       if (1 != (int)command.ExecuteNonQuery ())
+                                               throw new ProviderException ("failed to update Membership table");
+                               }
+
+                               trans.Commit ();
+                               return valid;
+                       }
+                       catch (ProviderException) {
+                               trans.Rollback ();
+                               throw;
+                       }
+                       catch (Exception e) {
+                               trans.Rollback ();
+                               throw new ProviderException ("error changing password", e);
+                       }
                }
                
-               [MonoTODO]
                public override bool ChangePasswordQuestionAndAnswer (string username, string password, string newPwdQuestion, string newPwdAnswer)
                {
-                       throw new NotImplementedException ();
+                       if (username != null) username = username.Trim ();
+                       if (newPwdQuestion != null) newPwdQuestion = newPwdQuestion.Trim ();
+                       if (newPwdAnswer != null) newPwdAnswer = newPwdAnswer.Trim ();
+
+                       CheckParam ("username", username, 256);
+                       if (RequiresQuestionAndAnswer)
+                               CheckParam ("newPwdQuestion", newPwdQuestion, 128);
+                       if (RequiresQuestionAndAnswer)
+                               CheckParam ("newPwdAnswer", newPwdAnswer, 128);
+
+                       MembershipUser user = GetUser (username, false);
+                       if (user == null) throw new ProviderException ("could not find user in membership database");
+                       if (user.IsLockedOut) throw new MembershipPasswordException ("user is currently locked out");
+
+                       InitConnection();
+
+                       DbTransaction trans = connection.BeginTransaction ();
+
+                       string commandText;
+                       DbCommand command;
+
+                       try {
+                               MembershipPasswordFormat passwordFormat;
+                               string db_salt;
+
+                               bool valid = ValidateUsingPassword (trans, username, password, out passwordFormat, out db_salt);
+                               if (valid) {
+
+                                       string db_passwordAnswer = EncodePassword (newPwdAnswer, passwordFormat, db_salt);
+
+                                       commandText = @"
+UPDATE m
+   SET PasswordQuestion = @PasswordQuestion,
+       PasswordAnswer = @PasswordAnswer,
+       FailedPasswordAttemptCount = 0,
+       FailedPasswordAttemptWindowStart = @DefaultDateTime,
+       FailedPasswordAnswerAttemptCount = 0,
+       FailedPasswordAnswerAttemptWindowStart = @DefaultDateTime
+  FROM dbo.aspnet_Membership m, dbo.aspnet_Users u, dbo.aspnet_Applications a
+ WHERE m.ApplicationId = a.ApplicationId
+   AND u.ApplicationId = a.ApplicationId
+   AND m.UserId = u.UserId
+   AND u.LoweredUserName = LOWER(@UserName)
+   AND a.LoweredApplicationName = LOWER(@ApplicationName)";
+
+                                       command = factory.CreateCommand ();
+                                       command.Transaction = trans;
+                                       command.CommandText = commandText;
+                                       command.Connection = connection;
+                                       command.CommandType = CommandType.Text;
+                                       AddParameter (command, "UserName", user.UserName);
+                                       AddParameter (command, "PasswordQuestion", newPwdQuestion);
+                                       AddParameter (command, "PasswordAnswer", db_passwordAnswer);
+                                       AddParameter (command, "ApplicationName", ApplicationName);
+                                       AddParameter (command, "DefaultDateTime", DefaultDateTime.ToString());
+
+                                       if (1 != (int)command.ExecuteNonQuery ())
+                                               throw new ProviderException ("failed to update Membership table");
+
+                               }
+
+                               trans.Commit ();
+                               return valid;
+                       }
+                       catch (ProviderException) {
+                               trans.Rollback ();
+                               throw;
+                       }
+                       catch (Exception e) {
+                               trans.Rollback ();
+                               throw new ProviderException ("error changing password question and answer", e);
+                       }
                }
                
                public override MembershipUser CreateUser (string username,
@@ -208,23 +358,13 @@ namespace System.Web.Security {
                        string passwordSalt = "";
 
                        RandomNumberGenerator rng = RandomNumberGenerator.Create ();
+                       byte[] salt = new byte[SALT_BYTES];
+                       rng.GetBytes (salt);
+                       passwordSalt = Convert.ToBase64String (salt);
 
-                       switch (PasswordFormat) {
-                       case MembershipPasswordFormat.Hashed:
-                               byte[] salt = new byte[16];
-                               rng.GetBytes (salt);
-                               passwordSalt = Convert.ToBase64String (salt);
-                               password = HashAndBase64Encode (password, salt);
-                               if (RequiresQuestionAndAnswer)
-                                       pwdAnswer = HashAndBase64Encode (pwdAnswer, salt);
-                               break;
-                       case MembershipPasswordFormat.Encrypted:
-                               password = EncryptAndBase64Encode (password);
-                               break;
-                       case MembershipPasswordFormat.Clear:
-                       default:
-                               break;
-                       }
+                       password = EncodePassword (password, PasswordFormat, passwordSalt);
+                       if (RequiresQuestionAndAnswer)
+                               pwdAnswer = EncodePassword (pwdAnswer, PasswordFormat, passwordSalt);
 
                        /* make sure the hashed/encrypted password and
                         * answer are still under 128 characters. */
@@ -509,13 +649,11 @@ DELETE dbo.aspnet_Users
                        }
                }
                
-               [MonoTODO]
                public virtual string GeneratePassword ()
                {
-                       throw new NotImplementedException ();
+                       return Membership.GeneratePassword (minRequiredPasswordLength, minRequiredNonAlphanumericCharacters);
                }
                
-               [MonoTODO]
                public override MembershipUserCollection FindUsersByEmail (string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
                {
                        CheckParam ("emailToMatch", emailToMatch, 256);
@@ -554,7 +692,6 @@ SELECT u.UserName, m.UserId, m.Email, m.PasswordQuestion, m.Comment, m.IsApprove
                        return c;
                }
 
-               [MonoTODO]
                public override MembershipUserCollection FindUsersByName (string nameToMatch, int pageIndex, int pageSize, out int totalRecords)
                {
                        CheckParam ("nameToMatch", nameToMatch, 256);
@@ -593,7 +730,6 @@ SELECT u.UserName, m.UserId, m.Email, m.PasswordQuestion, m.Comment, m.IsApprove
                        return c;
                }
                
-               [MonoTODO]
                public override MembershipUserCollection GetAllUsers (int pageIndex, int pageSize, out int totalRecords)
                {
                        if (pageIndex < 0)
@@ -660,59 +796,106 @@ SELECT u.UserName, m.UserId, m.Email, m.PasswordQuestion, m.Comment, m.IsApprove
                }
                
                
-               [MonoTODO]
                public override int GetNumberOfUsersOnline ()
                {
                        string commandText;
 
                        InitConnection();
 
-                       commandText = @"
+                       DateTime now = DateTime.Now.ToUniversalTime ();
+
+                       commandText = String.Format (@"
 SELECT COUNT (*)
   FROM dbo.aspnet_Membership m, dbo.aspnet_Applications a, dbo.aspnet_Users u
  WHERE m.ApplicationId = a.ApplicationId
    AND u.ApplicationId = a.ApplicationId
    AND m.UserId = u.UserId
-   AND DATEADD(m,@UserIsOnlineTimeWindow,dbo.aspnet_Users.LastActivityDate) >= GETDATE()
-   AND a.LoweredApplicationName = LOWER(@ApplicationName)";
+   AND DATEADD(minute,{0},u.LastActivityDate) >= @Now
+   AND a.LoweredApplicationName = LOWER(@ApplicationName)",
+                                                    userIsOnlineTimeWindow.Minutes);
 
                        DbCommand command = factory.CreateCommand ();
                        command.CommandText = commandText;
                        command.Connection = connection;
                        command.CommandType = CommandType.Text;
-                       AddParameter (command, "UserIsOnlineTimeWindow", userIsOnlineTimeWindow.Minutes.ToString());
+                       AddParameter (command, "Now", now.ToString ());
                        AddParameter (command, "ApplicationName", ApplicationName);
 
-                       try { 
-                               return (int)command.ExecuteScalar ();
-                       }
-                       catch {
-                               return -1;
-                       }
+                       return (int)command.ExecuteScalar ();
                }
                
-               [MonoTODO]
                public override string GetPassword (string username, string answer)
                {
-                       /* do the actual validation */
+                       if (!enablePasswordRetrieval)
+                               throw new NotSupportedException ("this provider has not been configured to allow the retrieval of passwords");
 
-                       /* if the validation succeeds:
-                          
-                          set LastLoginDate to DateTime.Now
-                          set FailedPasswordAnswerAttemptCount to 0
-                          set FailedPasswordAnswerAttemptWindowStart to DefaultDateTime
-                       */
+                       CheckParam ("username", username, 256);
+                       if (RequiresQuestionAndAnswer)
+                               CheckParam ("answer", answer, 128);
 
-                       /* if validation fails:
+                       MembershipUser user = GetUser (username, false);
+                       if (user == null) throw new ProviderException ("could not find user in membership database");
+                       if (user.IsLockedOut) throw new MembershipPasswordException ("user is currently locked out");
 
-                          if (FailedPasswordAnswerAttemptWindowStart - DateTime.Now < PasswordAttemptWindow)
-                            increment FailedPasswordAnswerAttemptCount
-                          FailedPasswordAnswerAttemptWindowStart = DateTime.Now
-                          if (FailedPasswordAnswerAttemptCount > MaxInvalidPasswordAttempts)
-                            set IsLockedOut = true.
-                            set LastLockoutDate = DateTime.Now
-                       */
-                       throw new NotImplementedException ();
+                       InitConnection();
+
+                       DbTransaction trans = connection.BeginTransaction ();
+
+                       try {
+                               MembershipPasswordFormat passwordFormat;
+                               string salt;
+                               string password = null;
+
+                               if (ValidateUsingPasswordAnswer (trans, username, answer,
+                                                                out passwordFormat, out salt)) {
+
+
+                                       /* if the validation succeeds:
+
+                                          set LastLoginDate to DateTime.Now
+                                          set FailedPasswordAnswerAttemptCount to 0
+                                          set FailedPasswordAnswerAttemptWindowStart to DefaultDateTime
+                                       */
+
+                                       string commandText = @"
+SELECT m.Password
+  FROM dbo.aspnet_Membership m, dbo.aspnet_Applications a, dbo.aspnet_Users u
+ WHERE m.ApplicationId = a.ApplicationId
+   AND u.ApplicationId = a.ApplicationId
+   AND m.UserId = u.UserId
+   AND u.LoweredUserName = LOWER(@UserName)
+   AND a.LoweredApplicationName = LOWER(@ApplicationName)";
+
+                                       DbCommand command = factory.CreateCommand ();
+                                       command.Transaction = trans;
+                                       command.CommandText = commandText;
+                                       command.Connection = connection;
+                                       command.CommandType = CommandType.Text;
+                                       AddParameter (command, "UserName", username);
+                                       AddParameter (command, "ApplicationName", ApplicationName);
+
+                                       DbDataReader reader = command.ExecuteReader ();
+                                       reader.Read ();
+                                       password = reader.GetString (0);
+                                       reader.Close();
+
+                                       password = DecodePassword (password, passwordFormat);
+                               }
+                               else {
+                                       throw new MembershipPasswordException ("The password-answer supplied is wrong.");
+                               }
+
+                               trans.Commit ();
+                               return password;
+                       }
+                       catch (MembershipPasswordException) {
+                               trans.Commit ();
+                               throw;
+                       }
+                       catch {
+                               trans.Rollback ();
+                               throw;
+                       }
                }
 
                MembershipUser GetUserFromReader (DbDataReader reader)
@@ -725,11 +908,11 @@ SELECT COUNT (*)
                                                   reader.IsDBNull (4) ? null : reader.GetString (4), /* comment */
                                                   reader.GetBoolean (5), /* isApproved */
                                                   reader.GetBoolean (6), /* isLockedOut */
-                                                  reader.GetDateTime (7), /* creationDate */
-                                                  reader.GetDateTime (8), /* lastLoginDate */
-                                                  reader.GetDateTime (9), /* lastActivityDate */
-                                                  reader.GetDateTime (10), /* lastPasswordChangedDate */
-                                                  reader.GetDateTime (11) /* lastLockoutDate */);
+                                                  reader.GetDateTime (7).ToLocalTime (), /* creationDate */
+                                                  reader.GetDateTime (8).ToLocalTime (), /* lastLoginDate */
+                                                  reader.GetDateTime (9).ToLocalTime (), /* lastActivityDate */
+                                                  reader.GetDateTime (10).ToLocalTime (), /* lastPasswordChangedDate */
+                                                  reader.GetDateTime (11).ToLocalTime () /* lastLockoutDate */);
                }
 
                MembershipUser BuildMembershipUser (DbCommand query, bool userIsOnline)
@@ -922,7 +1105,21 @@ SELECT u.UserName
                                rv = val;
                        return rv;
                }
-               
+
+               void EmitValidatingPassword (string username, string password, bool isNewUser)
+               {
+                       ValidatePasswordEventArgs args = new ValidatePasswordEventArgs (username, password, isNewUser);
+                       OnValidatingPassword (args);
+
+                       /* if we're canceled.. */
+                       if (args.Cancel) {
+                               if (args.FailureInformation == null)
+                                       throw new ProviderException ("Password validation canceled");
+                               else
+                                       throw args.FailureInformation;
+                       }
+               }
+
                public override void Initialize (string name, NameValueCollection config)
                {
                        if (config == null)
@@ -947,16 +1144,9 @@ SELECT u.UserName
                        
                        userIsOnlineTimeWindow = section.UserIsOnlineTimeWindow;
 
-                       /* come up with an init_vector for encryption algorithms */
-                       // IV is 8 bytes long for 3DES
-                       init_vector = new byte[8];
-                       int len = applicationName.Length;
-                       for (int i = 0; i < 8; i++) {
-                               if (i >= len)
-                                       break;
-
-                               init_vector [i] = (byte) applicationName [i];
-                       }
+                       /* we can't support password retrieval with hashed passwords */
+                       if (passwordFormat == MembershipPasswordFormat.Hashed && enablePasswordRetrieval)
+                               throw new ProviderException ("password retrieval cannot be used with hashed passwords");
 
                        string connectionStringName = config["connectionStringName"];
 
@@ -967,11 +1157,89 @@ SELECT u.UserName
 
                        connectionString = WebConfigurationManager.ConnectionStrings[connectionStringName];
                }
-               
-               [MonoTODO]
+
                public override string ResetPassword (string username, string answer)
                {
-                       throw new NotImplementedException ();
+                       if (!enablePasswordReset)
+                               throw new NotSupportedException ("this provider has not been configured to allow the resetting of passwords");
+
+                       CheckParam ("username", username, 256);
+                       if (RequiresQuestionAndAnswer)
+                               CheckParam ("answer", answer, 128);
+
+                       MembershipUser user = GetUser (username, false);
+                       if (user == null) throw new ProviderException ("could not find user in membership database");
+                       if (user.IsLockedOut) throw new MembershipPasswordException ("user is currently locked out");
+
+                       InitConnection();
+
+                       string commandText;
+                       DbCommand command;
+
+                       DbTransaction trans = connection.BeginTransaction ();
+
+                       try {
+                               MembershipPasswordFormat db_passwordFormat;
+                               string db_salt;
+                               string newPassword = null;
+
+                               if (ValidateUsingPasswordAnswer (trans, user.UserName, answer, out db_passwordFormat, out db_salt)) {
+
+                                       newPassword = GeneratePassword ();
+                                       string db_password;
+
+                                       EmitValidatingPassword (username, newPassword, false);
+
+                                       /* otherwise update the user's password in the db */
+
+                                       db_password = EncodePassword (newPassword, db_passwordFormat, db_salt);
+
+                                       commandText = @"
+UPDATE m
+   SET Password = @Password,
+       FailedPasswordAnswerAttemptCount = 0,
+       FailedPasswordAnswerAttemptWindowStart = @DefaultDateTime
+  FROM dbo.aspnet_Membership m, dbo.aspnet_Users u, dbo.aspnet_Applications a
+ WHERE m.ApplicationId = a.ApplicationId
+   AND u.ApplicationId = a.ApplicationId
+   AND m.UserId = u.UserId
+   AND u.LoweredUserName = LOWER(@UserName)
+   AND a.LoweredApplicationName = LOWER(@ApplicationName)";
+
+                                       command = factory.CreateCommand ();
+                                       command.Transaction = trans;
+                                       command.CommandText = commandText;
+                                       command.Connection = connection;
+                                       command.CommandType = CommandType.Text;
+                                       AddParameter (command, "UserName", user.UserName);
+                                       AddParameter (command, "Password", db_password);
+                                       AddParameter (command, "ApplicationName", ApplicationName);
+                                       AddParameter (command, "DefaultDateTime", DefaultDateTime.ToString());
+
+                                       if (1 != (int)command.ExecuteNonQuery ())
+                                               throw new ProviderException ("failed to update Membership table");
+
+                                       trans.Commit ();
+                               }
+                               else {
+                                       throw new MembershipPasswordException ("The password-answer supplied is wrong.");
+                               }
+
+                               return newPassword;
+                       }
+                       catch (MembershipPasswordException) {
+                               trans.Commit ();
+                               throw;
+                       }
+                       catch (ProviderException) {
+                               trans.Rollback ();
+                               throw;
+                       }
+                       catch (Exception e) {
+                               trans.Rollback ();
+
+                               throw new ProviderException ("Failed to reset password", e);
+                       }
                }
                
                public override void UpdateUser (MembershipUser user)
@@ -1049,9 +1317,9 @@ UPDATE dbo.aspnet_Users
 
                                trans.Commit ();
                        }
-                       catch (ProviderException e) {
+                       catch (ProviderException) {
                                trans.Rollback ();
-                               throw e;
+                               throw;
                        }
                        catch (Exception e) {
                                trans.Rollback ();
@@ -1059,7 +1327,6 @@ UPDATE dbo.aspnet_Users
                        }
                }
                
-               [MonoTODO ("flesh out the case where validation fails")]
                public override bool ValidateUser (string username, string password)
                {
                        MembershipUser user = GetUser (username, false);
@@ -1072,67 +1339,22 @@ UPDATE dbo.aspnet_Users
                        if (!user.IsApproved)
                                return false;
 
-                       ValidatePasswordEventArgs args = new ValidatePasswordEventArgs (username, password, false);
-                       OnValidatingPassword (args);
+                       EmitValidatingPassword (username, password, false);
 
-                       if (args.Cancel)
-                               throw new ProviderException ("Password validation failed");
-                       if (args.FailureInformation != null)
-                               throw args.FailureInformation;
-
-                       /* get the password/salt from the db */
-                       string db_password;
-                       MembershipPasswordFormat db_passwordFormat;
-                       string db_salt;
+                       InitConnection();
 
                        DbTransaction trans = connection.BeginTransaction ();
 
                        string commandText;
                        DbCommand command;
 
-                       InitConnection();
-
                        try {
-                               commandText = @"
-SELECT m.Password, m.PasswordFormat, m.PasswordSalt
-  FROM dbo.aspnet_Membership m, dbo.aspnet_Applications a, dbo.aspnet_Users u
- WHERE m.ApplicationId = a.ApplicationId
-   AND u.ApplicationId = a.ApplicationId
-   AND m.UserId = u.UserId
-   AND u.LoweredUserName = LOWER(@UserName)
-   AND a.LoweredApplicationName = LOWER(@ApplicationName)";
-
-                               command = factory.CreateCommand ();
-                               command.Transaction = trans;
-                               command.CommandText = commandText;
-                               command.Connection = connection;
-                               command.CommandType = CommandType.Text;
-                               AddParameter (command, "UserName", user.UserName);
-                               AddParameter (command, "ApplicationName", ApplicationName);
-
-                               DbDataReader reader = command.ExecuteReader ();
-                               reader.Read ();
-                               db_password = reader.GetString (0);
-                               db_passwordFormat = (MembershipPasswordFormat)reader.GetInt32 (1);
-                               db_salt = reader.GetString (2);
-                               reader.Close();
-
-                               /* do the actual validation */
-                               switch (db_passwordFormat) {
-                               case MembershipPasswordFormat.Hashed:
-                                       byte[] salt = Convert.FromBase64String (db_salt);
-                                       password = HashAndBase64Encode (password, salt);
-                                       break;
-                               case MembershipPasswordFormat.Encrypted:
-                                       password = EncryptAndBase64Encode (password);
-                                       break;
-                               case MembershipPasswordFormat.Clear:
-                                       break;
-                               }
-
-                               bool valid = (password == db_password);
+                               MembershipPasswordFormat passwordFormat;
+                               string salt;
 
+                               bool valid = ValidateUsingPassword (trans, username, password, out passwordFormat, out salt);
                                if (valid) {
+
                                        DateTime now = DateTime.Now.ToUniversalTime ();
 
                                        /* if the validation succeeds:
@@ -1190,16 +1412,6 @@ UPDATE dbo.aspnet_Users
                                        if (1 != (int)command.ExecuteNonQuery ())
                                                throw new ProviderException ("failed to update User table");
                                }
-                               else {
-                                       /* if validation fails:
-                                          if (FailedPasswordAttemptWindowStart - DateTime.Now < PasswordAttemptWindow)
-                                            increment FailedPasswordAttemptCount
-                                          FailedPasswordAttemptWindowStart = DateTime.Now
-                                          if (FailedPasswordAttemptCount > MaxInvalidPasswordAttempts)
-                                            set IsLockedOut = true.
-                                            set LastLockoutDate = DateTime.Now
-                                       */
-                               }
 
                                trans.Commit ();
 
@@ -1208,24 +1420,29 @@ UPDATE dbo.aspnet_Users
                        catch {
                                trans.Rollback ();
 
-                               return false; /* should we allow the exception through? */
+                               throw;
                        }
                }
 
-               [MonoTODO]
                public override bool UnlockUser (string userName)
                {
-                       string commandText = @"
-UPDATE dbo.aspnet_Membership, dbo.aspnet_Users, dbo.aspnet_Application
-   SET dbo.aspnet_Membership.IsLockedOut = 0
- WHERE dbo.aspnet_Membership.UserId = dbo.aspnet_Users.UserId
-   AND dbo.aspnet_Membership.ApplicationId = dbo.aspnet_Applications.ApplicationId
-   AND dbo.aspnet_Users.ApplicationId = dbo.aspnet_Applications.ApplicationId
-   AND dbo.aspnet_Users.LoweredUserName = LOWER (@UserName)
-   AND dbo.aspnet_Applications.LoweredApplicationName = LOWER(@ApplicationName)";
-
                        CheckParam ("userName", userName, 256);
 
+                       string commandText = @"
+UPDATE dbo.aspnet_Membership
+   SET IsLockedOut = 0,
+       LastLockoutDate = @DefaultDateTime,
+       FailedPasswordAttemptCount = 0,
+       FailedPasswordAttemptWindowStart = @DefaultDateTime,
+       FailedPasswordAnswerAttemptCount = 0,
+       FailedPasswordAnswerAttemptWindowStart = @DefaultDateTime
+  FROM dbo.aspnet_Membership m, dbo.aspnet_Users u, dbo.aspnet_Applications a
+ WHERE m.UserId = u.UserId
+   AND m.ApplicationId = a.ApplicationId
+   AND u.ApplicationId = a.ApplicationId
+   AND u.LoweredUserName = LOWER (@UserName)
+   AND a.LoweredApplicationName = LOWER(@ApplicationName)";
+
                        InitConnection();
 
                        DbCommand command = factory.CreateCommand ();
@@ -1234,15 +1451,197 @@ UPDATE dbo.aspnet_Membership, dbo.aspnet_Users, dbo.aspnet_Application
                        command.CommandType = CommandType.Text;
                        AddParameter (command, "UserName", userName);
                        AddParameter (command, "ApplicationName", ApplicationName);
+                       AddParameter (command, "DefaultDateTime", DefaultDateTime.ToString());
 
-                       try {
-                               return command.ExecuteNonQuery() == 1;
+                       return command.ExecuteNonQuery() == 1;
+               }
+
+               void IncrementFailureAndMaybeLockout (DbTransaction trans, string username,
+                                                     string failureCountAttribute, string failureWindowAttribute)
+               {
+                       DateTime now = DateTime.Now;
+
+                       /* if validation fails:
+                          if (FailedPasswordAttemptWindowStart - DateTime.Now < PasswordAttemptWindow)
+                            increment FailedPasswordAttemptCount
+                          FailedPasswordAttemptWindowStart = DateTime.Now
+                          if (FailedPasswordAttemptCount > MaxInvalidPasswordAttempts)
+                            set IsLockedOut = true.
+                            set LastLockoutDate = DateTime.Now
+                       */
+
+                       string commandText = String.Format (@"
+SELECT m.{0}, m.{1}
+  FROM dbo.aspnet_Membership m, dbo.aspnet_Applications a, dbo.aspnet_Users u
+ WHERE m.ApplicationId = a.ApplicationId
+   AND u.ApplicationId = a.ApplicationId
+   AND m.UserId = u.UserId
+   AND u.LoweredUserName = LOWER(@UserName)
+   AND a.LoweredApplicationName = LOWER(@ApplicationName)",
+                                                    failureCountAttribute, failureWindowAttribute);
+
+                       DbCommand command = factory.CreateCommand ();
+                       command.Transaction = trans;
+                       command.CommandText = commandText;
+                       command.Connection = connection;
+                       command.CommandType = CommandType.Text;
+                       AddParameter (command, "UserName", username);
+                       AddParameter (command, "ApplicationName", ApplicationName);
+
+                       DateTime db_FailedWindowStart;
+                       int db_FailedCount;
+
+                       DbDataReader reader = command.ExecuteReader ();
+                       reader.Read ();
+                       db_FailedCount = reader.GetInt32 (0);
+                       db_FailedWindowStart = reader.GetDateTime (1).ToLocalTime ();
+                       reader.Close();
+
+                       TimeSpan diff = now.Subtract (db_FailedWindowStart);
+                       if ((db_FailedWindowStart == DefaultDateTime.ToLocalTime ())
+                           || diff.Minutes < PasswordAttemptWindow)
+                               db_FailedCount ++;
+
+                       if (db_FailedCount > MaxInvalidPasswordAttempts) {
+                               /* lock the user out */
+                               commandText = @"
+UPDATE dbo.aspnet_Membership
+   SET IsLockedOut = 1,
+       LastLockoutDate = @LastLockoutDate
+  FROM dbo.aspnet_Membership m, dbo.aspnet_Users u, dbo.aspnet_Applications a
+ WHERE m.ApplicationId = a.ApplicationId
+   AND u.ApplicationId = a.ApplicationId
+   AND m.UserId = u.UserId
+   AND u.LoweredUserName = LOWER(@UserName)
+   AND a.LoweredApplicationName = LOWER(@ApplicationName)";
+
+                               command = factory.CreateCommand ();
+                               command.Transaction = trans;
+                               command.CommandText = commandText;
+                               command.Connection = connection;
+                               command.CommandType = CommandType.Text;
+                               AddParameter (command, "UserName", username);
+                               AddParameter (command, "ApplicationName", ApplicationName);
+                               AddParameter (command, "LastLockoutDate", now.ToUniversalTime().ToString ());
                        }
-                       catch {
-                               return false;
+                       else {
+                               /* just store back the updated window start and count */
+                               commandText = String.Format (@"
+UPDATE dbo.aspnet_Membership
+   SET {0} = @{0},
+       {1} = @{1}
+  FROM dbo.aspnet_Membership m, dbo.aspnet_Users u, dbo.aspnet_Applications a
+ WHERE m.ApplicationId = a.ApplicationId
+   AND u.ApplicationId = a.ApplicationId
+   AND m.UserId = u.UserId
+   AND u.LoweredUserName = LOWER(@UserName)
+   AND a.LoweredApplicationName = LOWER(@ApplicationName)",
+                                                    failureCountAttribute, failureWindowAttribute);
+
+                               command = factory.CreateCommand ();
+                               command.Transaction = trans;
+                               command.CommandText = commandText;
+                               command.Connection = connection;
+                               command.CommandType = CommandType.Text;
+                               AddParameter (command, "UserName", username);
+                               AddParameter (command, "ApplicationName", ApplicationName);
+                               AddParameter (command, failureCountAttribute, db_FailedCount.ToString());
+                               AddParameter (command, failureWindowAttribute, now.ToUniversalTime().ToString ());
                        }
+
+                       if (1 != (int)command.ExecuteNonQuery ())
+                               throw new ProviderException ("failed to update Membership table");
                }
-               
+
+               bool ValidateUsingPassword (DbTransaction trans, string username, string password,
+                                           out MembershipPasswordFormat passwordFormat,
+                                           out string salt)
+               {
+                       string commandText = @"
+SELECT m.Password, m.PasswordFormat, m.PasswordSalt
+  FROM dbo.aspnet_Membership m, dbo.aspnet_Applications a, dbo.aspnet_Users u
+ WHERE m.ApplicationId = a.ApplicationId
+   AND u.ApplicationId = a.ApplicationId
+   AND m.UserId = u.UserId
+   AND u.LoweredUserName = LOWER(@UserName)
+   AND a.LoweredApplicationName = LOWER(@ApplicationName)";
+
+                       DbCommand command = factory.CreateCommand ();
+                       command.Transaction = trans;
+                       command.CommandText = commandText;
+                       command.Connection = connection;
+                       command.CommandType = CommandType.Text;
+                       AddParameter (command, "UserName", username);
+                       AddParameter (command, "ApplicationName", ApplicationName);
+
+                       string db_password;
+
+                       DbDataReader reader = command.ExecuteReader ();
+                       reader.Read ();
+                       db_password = reader.GetString (0);
+                       passwordFormat = (MembershipPasswordFormat)reader.GetInt32 (1);
+                       salt = reader.GetString (2);
+                       reader.Close();
+
+                       /* do the actual validation */
+                       password = EncodePassword (password, passwordFormat, salt);
+
+                       bool valid = (password == db_password);
+
+                       if (!valid)
+                               IncrementFailureAndMaybeLockout (trans, username,
+                                                                "FailedPasswordAttemptCount", "FailedPasswordAttemptWindowStart");
+
+                       return valid;
+               }
+
+
+               bool ValidateUsingPasswordAnswer (DbTransaction trans, string username, string answer,
+                                                 out MembershipPasswordFormat passwordFormat,
+                                                 out string salt)
+               {
+                       string commandText = @"
+SELECT m.PasswordAnswer, m.PasswordFormat, m.PasswordSalt
+  FROM dbo.aspnet_Membership m, dbo.aspnet_Applications a, dbo.aspnet_Users u
+ WHERE m.ApplicationId = a.ApplicationId
+   AND u.ApplicationId = a.ApplicationId
+   AND m.UserId = u.UserId
+   AND u.LoweredUserName = LOWER(@UserName)
+   AND a.LoweredApplicationName = LOWER(@ApplicationName)";
+
+                       DbCommand command = factory.CreateCommand ();
+                       command.Transaction = trans;
+                       command.CommandText = commandText;
+                       command.Connection = connection;
+                       command.CommandType = CommandType.Text;
+                       AddParameter (command, "UserName", username);
+                       AddParameter (command, "ApplicationName", ApplicationName);
+
+                       string db_answer;
+
+                       DbDataReader reader = command.ExecuteReader ();
+                       reader.Read ();
+                       db_answer = reader.GetString (0);
+                       passwordFormat = (MembershipPasswordFormat)reader.GetInt32 (1);
+                       salt = reader.GetString (2);
+                       reader.Close();
+
+                       /* do the actual password answer check */
+                       answer = EncodePassword (answer, passwordFormat, salt);
+
+                       if (answer.Length > 128)
+                               throw new ArgumentException (String.Format ("password answer hashed to longer than 128 characters"));
+
+                       bool valid = (answer == db_answer);
+
+                       if (!valid)
+                               IncrementFailureAndMaybeLockout (trans, username,
+                                                                "FailedPasswordAnswerAttemptCount",
+                                                                "FailedPasswordAnswerAttemptWindowStart");
+
+                       return valid;
+               }
+
                [MonoTODO]
                public override string ApplicationName {
                        get { return applicationName; }