As a follow-on to my last post on using FBA sites in SharePoint, I wanted to point out an additional enhancement you can make, which will help out your users.

When a user tries to log into your Web site using the Login control, unfortunately, it often just tells the user a bland message such as “invalid login”. That doesn’t give the user enough information. Is their username wrong? Is the password wrong? Are the username and password correct but the user’s been locked out? Users have no idea what the true problem is. I found a way around this by manually validating the user, discovering any error messages, and sending those to the user, and I’d like to show you how to do this as well.

The most difficult part of this is that if a user is using the wrong password, your Membership Provider will increment a field called FailedPasswordAttemptCount in the Membership table in your database. Once the user has reached the maximum number of failed attempts (as specified by the MaximumInvalidPasswordAttempts attribute of your provider node), the user’s account will be locked.

If you use a method such as Membership.ValidateUser(), that field will be queried in the database. Unfortunately, though, I don’t know of a good way to access that value directly from the MembershipUser object, the way you can find out things like MembershipUser.IsLockedOut.

To get around this, I simply created a new stored procedure that I added to my membership database. (I’m not using multiple application names in my web application, so I’m doing a “poor man’s” query of simply retrieving a user by the user name they typed into the login box.) Here’s the stored procedure:

CREATE PROCEDURE [dbo].[aspnet_Membership_GetFailedPasswordAttemptCount]
    @UserName  nvarchar(256)
AS
DECLARE @UserId  uniqueidentifier
DECLARE @FailedPasswordAttemptCount  int

SELECT  @UserId = u.UserId
FROM    dbo.aspnet_Users u
WHERE   @UserName = u.LoweredUserName

SELECT FailedPasswordAttemptCount
FROM aspnet_Membership<br> WHERE UserId = @UserId

Then, I simply added a method that executes this stored procedure:

private int GetFailedPasswordAttemptCount(string userName)
{
    SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["FBAConnectionString"].ConnectionString);
    SqlCommand cmd = new SqlCommand("aspnet_Membership_GetFailedPasswordAttemptCount", conn);
    cmd.CommandType = System.Data.CommandType.StoredProcedure;
    cmd.Parameters.Add("@Username", System.Data.SqlDbType.VarChar, 255).Value = userName;
    int failedAttempts = 0;
    try
    {
        conn.Open();
        failedAttempts = (int)cmd.ExecuteScalar();
        conn.Close();
    }
    catch
    {
        throw;
    }
    finally
    {
        if (conn.State != System.Data.ConnectionState.Closed)
        {
            conn.Close();
        }
    }

    return failedAttempts;
}

Finally, I create a custom event handler for the Authenticate event on the ASP.NET Login control I’m using.

public void loginCtrl_Authenticate(object sender, AuthenticateEventArgs e)
{
    //Manually validate the user. If they're valid, then they authenticate just fine and get logged in.
    bool isValid = Membership.ValidateUser(loginCtrl.UserName, loginCtrl.Password);
    if (isValid)
    {
        e.Authenticated = true;
    }
    else
    {
        MembershipUser user = Membership.GetUser(loginCtrl.UserName);

        //First, check to see if the username was found in the Membership database
        if (user != null)
        {
            //Next, find out if the user has been locked out.
            if (user.IsLockedOut)
            {
                //If the user's account is locked out, notify them.
                e.Authenticated = false;
                loginCtrl.FailureText = "Your account is locked out. Please contact an administrator.";
            }
            //If the user isn't locked out, it means their password is wrong. Notify them how many times they have entered a false password, and how many times they can have a failed login before they're locked out.
            else
            {
                e.Authenticated = false;
                int failedAttempts = GetFailedPasswordAttemptCount(user.UserName);
                string errorMsg = "Your password is incorrect. Your login has failed {0} times. After {1} failed logins you will be locked out.";
                loginCtrl.FailureText = String.Format(errorMsg, failedAttempts, Membership.MaxInvalidPasswordAttempts);
            }
        }
        else
        {
            e.Authenticated = false;
            loginCtrl.FailureText = "The username cannot be found.";
        }
    }
}

Finally, simple register the event handler with the Login control’s Authenticate event:

loginCntrl.Authenticate += new AuthenticateEventHandler(loginCtrl_Authenticate);

In my case, my Login control is hosted in a Web Part. Even if your control is in an ASP.NET login page you’ve created, you’ll simply add the last several methods to whatever control or page is hosting your Login control.