Joe Piccirilli

Subscribe to Joe Piccirilli: eMailAlertsEmail Alerts
Get Joe Piccirilli: homepageHomepage mobileMobile rssRSS facebookFacebook twitterTwitter linkedinLinkedIn


Article

Customizing the .NET SQL Membership Provider

How to customize the behavior of the provider using very little code

With the release of ASP.NET 2.0, Microsoft introduced Web developers to the "provider" model that addresses common application infrastructure needs using a system of pluggable modules adhering to common interfaces.

ASP.NET shipped with modules to cover Membership (authentication), Roles (authorization), Sitemap (navigation), and others. These modules work with each other, and with many of the controls included in Visual Studio, to drastically reduce the amount of code needed to perform the functions that nearly every Web application requires.

Each provider shipped with multiple implementations to cover the most common development and deployment scenarios. More importantly, the providers were built in a way that lets developers customize key elements of functionality without having to write an entire provider from scratch and without having to worry about any other code that consumes it.

I will be talking specifically about the SQL Membership provider that handles authentication and user management functionality using a SQL Server database as the membership repository. I'll provide examples of how to customize the behavior of the provider using very little code.

Anyone developing membership-based Web sites has likely found himself writing code to manage users. Those who are industrious enough (or simply tired of the pain) may take it one step further and write more generic code that can be plugged into different sites with only minor modifications. Even after putting in that effort, you often find yourself having to modify the code and user interface components that consume the membership data, such as screens for logging in, editing user profiles, retrieving passwords, etc. It was with this hardship in mind that the SQL Membership Provider was born.

Using Visual Studio and a simple command-line command, a Web site can be created that provides functionality and user interface components for authentication, profile editing, and password management capabilities using SQL Server 2005 or SQL Server Express as a data store. Passwords are secured by being stored hashed or encrypted, by enforcing a "password lockout" policy, and by requiring security questions and answers to reset. The best part is that all of this functionality can be provided without the developer writing a single line of code.

There are not many cases where "one size fits all," and membership functionality certainly falls into that category. Fortunately, customizing the Membership Provider is almost as simple as using its default configuration. Basic customization can be handled in the configuration file for the Web application, still not requiring any user-written code. For instance, you can configure whether a security question must be answered before a password is reset by setting the requiresQuestionAndAnswer attribute of the membership provider's "add" element. The PasswordRecovery control then works with the membership provider and adjusts its user interface by adding steps to the password recovery "wizard" to prompt for the security question configured by the user and to validate the answer provided.

If the application needs to draw on information from a proprietary source, such as a company's internal user management application, a custom membership provider can be created. When implementing a membership provider, the developer is responsible for, and has complete control over, all aspects of the process. From connectivity to the data source, to password protection, to creating new users; each step requires explicit implementation. Fortunately, once completed, the provider can be plugged into the Web site via the Web configuration and will work seamlessly with the existing membership controls. While this method offers a great deal of control and flexibility, it requires a fair amount of effort for someone who needs only minor functionality changes that are not configurable using the existing providers. Thanks to the provider model and object-oriented programming, there's a much easier middle ground.

To demonstrate the ease with which a Membership Provider can be customized, it's best to consider a real-world example. The standard SQL Membership Provider has a great deal of functionality to protect and manage Web site passwords. With nothing more than configuration, passwords can be protected and stored using a one-way hash so that even database administrators cannot retrieve them. To grant access to a user who has forgotten his password, a new password must be created. This is typically done via the Password Reset Web Control, which implicitly uses the ResetPassword method of the SQL Membership Provider to auto-generate a new password and send it to the user in an e-mail message.

Your password has been reset. Please return to the site and log in using the following information.
User Name: JoePiccirilli
Password: :mits(*^w?C[@m

As you can see, the default reset password functionality creates a strong password based on random characters that include letters, numbers, and special characters. While this implementation offers great protection, it also has many usability issues. For a user to type this password, he has to distinguish between characters that appear similar (1 vs. I, 0 vs. O, etc.) Some users might even be challenged to simply find the characters in question. Even the option of copy-and-paste falls short if you aren't careful. The default selection logic of many mail clients will try to select all of the characters after and excluding the leading colon ":" in the password above. After too many invalid attempts, the user could be locked out of the site, causing more frustration. The Membership Provider gives you some control over passwords, such as minimum password length, minimum required non-alphanumeric characters; even a password-strength regular expression. If you want to allow strong passwords while resetting to more basic passwords, you need to move beyond the native capabilities of the SQL Membership Provider.

To customize the SQL Membership Provider, you start by creating a new class that inherits from the existing SQLMembershipProvider class.

public class MembershipProvider : SQLMembershipProvider

This gives you all of the existing functionality and the hooks you need to implement your own variations. In our example, we'll want to create a list of "valid" characters to be included in our new password, and a Random object to mix it up. We'll also want to set how long we want the password to be.

/// Create an array of characters to user for password reset.
/// Exclude confusing or ambiguous characters such as 1 0 l o i
string[] characters = new string[] { "2", "3", "4", "5", "6", "7", "8",
"9", "a", "b", "c", "d", "e", "f", "g", "h", "j", "k", "m", "n",
"p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"};

const int DefaultResetPasswordLength = 10;
private int ResetPasswordLength;

To get the SQL Membership Provider to use our more friendly characters, we have to override the ResetPassword method. We'll do this by creating an empty string and appending random characters from the string array, repeated based on the desired password length.

// Create a more user friendly password to avoid confusion when
// trying to key in the new value
public override string GeneratePassword()
{
string newPassword = string.Empty;
System.Random rnd = new Random();

for (int i = 0; i < ResetPasswordLength; i++)
{
newPassword +=
characters[rnd.Next(characters.GetUpperBound(0))];
}
return newPassword;
}

When ASP.NET's Password Reset control (or any other code using the provider) calls the PasswordReset method, this code will execute instead of the default implementation. Meanwhile, all of the other functionality is still in place and working as expected.

To take the example a step further, you may decide that you want to give users control over some aspects of how the password is reset, such as the new password length. Just like the default implementation, attributes set in the Web configuration file can drive functionality in the customized provider.

You may have noticed the private fields DefaultResetPasswordLength (set to 10), and ResetPasswordLength (not set), while the GeneratePassword method uses ResetPasswordLength. To use custom attributes for your provider, you need to override the Initialize method.

public override void Initialize(string name,
System.Collections.Specialized.NameValueCollection config)
{
ResetPasswordLength = DefaultResetPasswordLength;

string ResetPasswordLengthConfig = config["resetPasswordLength"];
if (ResetPasswordLengthConfig != null)
{
// Have to remove the config entry as the provider we are
// inheriting from doesn't understand it and will throw an
// exception
config.Remove("resetPasswordLength");

if (!int.TryParse(ResetPasswordLengthConfig,
out ResetPasswordLength))
{
// Have to reset to default as TryParse will set it to 0
// if the value can't be parsed
ResetPasswordLength = DefaultResetPasswordLength;
}
}

base.Initialize(name, config);
}

Here, we see that the initialize method takes a NameValueCollection parameter that contains all of the attributes on the provider's Add element in the appropriate configuration file. You can extract a value from the name value collection, check to ensure it's there, and cast it to the appropriate data type for later use.

Note the "config.Remove" call in the middle of the method. Since we are calling the base class's Initialize method to get the default functionality, we need to remove any values that it doesn't understand to avoid an exception. This approach provides a reasonable default implementation, while giving the user control over some of the extended functionality. You can add additional configuration options, such as a custom string of acceptable characters, using the same approach.

To use the new provider, you need to get the assembly into your Web applications path. The easiest way to do this in a Web application is to set a reference to the project containing your custom provider. You also have to configure your Web site to use the custom provider. This can be done in any scope from Machine to Application, meaning you can define this provider in the root Web.config (in the appropriate framework version's config folder), or in the Web.config at the root of your Web site. It can't be configured in lower-level folders in the same Web application.

<connectionStrings>
<add
name="CustomSite_AspNetDB"
connectionString="Data Source=.;Initial Catalog=aspnetdb;"
/>
</connectionStrings>

<membership defaultProvider="CustomSiteMembershipProvider">
<providers>
<add
name="CustomSiteMembershipProvider"
type="CustomSQLMembershipProvider.MembershipProvider"
connectionStringName="CustomSite_AspNetDB"
applicationName="CustomSite"
minRequiredPasswordLength="6"
minRequiredNonalphanumericCharacters="0"
requiresQuestionAndAnswer="true"
requiresUniqueEmail="false"
resetPasswordLength="8"
/>
</providers>
</membership>

Another real-world example is the desire to have case-insensitive passwords. Although the SQL Server database may be set to be case insensitive, since the passwords are validated by comparing the hash of the original password to the hash of the attempted password, case sensitivity still applies. One possible approach to this can be accomplished by further modifying the SQL Membership Provider to "fake" case insensitivity by forcing both the original password and the password used to authenticate to lower case.

To make this customization, the three methods that use passwords, CreateUser, ChagePassword, and ValidateUser, must be overridden.

When creating a new user, force the password to lower case.

public override MembershipUser CreateUser(string username,
string password, string email, string passwordQuestion,
string passwordAnswer, bool isApproved, object providerUserKey,
out MembershipCreateStatus status)
{
if (!CaseSensitive)
{
password = password.ToLower();
}
return base.CreateUser(username, password, email, passwordQuestion,
passwordAnswer, isApproved, providerUserKey, out status);
}

Since the old password is required (and validated) to change to a new password, you need to force both to lower case when changing a password.

public override bool ChangePassword(string username,
string oldPassword, string newPassword)
{
if (!CaseSensitive)
{
oldPassword = oldPassword.ToLower();
newPassword = newPassword.ToLower();
}
return base.ChangePassword(username, oldPassword,
newPassword);
}

Finally, when validating the password during authentication, you need to ToLower the password being tested.

public override bool ValidateUser(string username, string password)
{
if (!CaseSensitive)
{
password = password.ToLower();
}
return base.ValidateUser(username, password);
}

As with the earlier example, most of the hard work is still being done by the default implementation, leaving you with only the custom logic to code.

The CaseSensitive value is set during initialization the same way the ResetPasswordLength value is.

One thing to pay special attention to with this customization is that it is only giving the appearance of case insensitivity. If there are any existing passwords with mixed or upper casing, login will fail when case insensitivity is enabled. This approach works only with new implementations, or where you have access to the original passwords to force them to lower case during a migration.

The SQL Membership Provider included with Microsoft's ASP.NET platform and Visual Studio takes many of the repetitive tasks off of the "to do" list when building Web applications. The Login Controls provide common user interface elements that work hand-in-hand with the underlying functionality implemented by the Provider. A developer needs only set properties on the controls, or change attributes on a configuration entry to vary the membership functionality, while maintaining the stability and security of the Web site. When customization needs exceed the configuration capabilities of the default Providers, developers can implement their own functionality and know that everything else surrounding it will continue to work seamlessly. For those whose needs do not warrant a completely customized provider, extending the existing provider can offer the best of both worlds.

<configuration>
<connectionStrings>
<add name="SQLServices" connectionString="Data Source=localhost; Integrated Security=SSPI;Initial Catalog=aspnetdb;" />
</connectionStrings>
<system.Web>
<authentication mode="Forms" >
<forms loginUrl="login.aspx"
name=".ASPXFORMSAUTH" />
</authentication>
<authorization>
<deny users="?" />
</authorization>
<membership defaultProvider="SQLProvider"
userIsOnlineTimeWindow="15">
<providers>
<add
name="SQLProvider"
type="System.Web.Security.SQLMembershipProvider"
connectionStringName="SQLServices"
applicationName="MyApplication"
enablePasswordRetrieval="false"
enablePasswordReset="true"
requiresQuestionAndAnswer="true"
requiresUniqueEmail="false"
passwordFormat="Hashed"
maxInvalidPasswordAttempts="5"
passwordAttemptWindow="10" />
</providers>
</membership>
</system.Web>
</configuration>

Resources

More Stories By Joe Piccirilli

Joe Piccirilli is director of the .NET Practice at Syrinx Consulting, where he works with enterprise clients on Microsoft .NET projects developing Windows, Web, SharePoint, and Office-based applications. He provides architectural guidance, insight into emerging tools and technologies, and operational and technical assistance on high-priority projects. Throughout his 12 years in IT, he has led and been a part of teams of all sizes, and through all phases of project life cycles.

Comments (1) View Comments

Share your thoughts on this story.

Add your comment
You must be signed in to add a comment. Sign-in | Register

In accordance with our Comment Policy, we encourage comments that are on topic, relevant and to-the-point. We will remove comments that include profanity, personal attacks, racial slurs, threats of violence, or other inappropriate material that violates our Terms and Conditions, and will block users who make repeated violations. We ask all readers to expect diversity of opinion and to treat one another with dignity and respect.


Most Recent Comments
orouit 08/20/09 09:39:00 PM EDT

Very interesting article. I would have a simple question. Is the MembershipProvider using a custom set of tables or the one of Oracle Data directory?

Asking this to know if it can be used directly with the Oracle users identity or has to be rewritten to take advantage of this.