Membership Providers

 

Introduction to the Provider Model
Membership Providers
Role Providers
Site Map Providers
Session State Providers
Profile Providers
Web Event Providers
Web Parts Personalization Providers
Custom Provider-Based Services
Hands-on Custom Providers: The Contoso Times

Membership providers provide the interface between ASP.NET's membership service and membership data sources. The two most common reasons for writing a custom membership provider are:

  • You wish to store membership information in a data source that is not supported by the membership providers included with the .NET Framework, such as an Oracle database or a Web service.
  • You wish to store membership information in a SQL Server database whose schema differs from that of the database used by System.Web.Security.SqlMembershipProvider—if, for example, you need to integrate ASP.NET's membership service with an existing membership database.

The fundamental job of a membership provider is to interface with data sources containing data regarding a site's registered users, and to provide methods for creating users, deleting users, verifying login credentials, changing passwords, and so on. The .NET Framework's System.Web.Security namespace includes a class named MembershipUser that defines the basic attributes of a membership user and that a membership provider uses to represent individual users.

The MembershipProvider Class

Developers writing custom membership providers begin by deriving from System.Web.Security.MembershipProvider, which derives from ProviderBase and adds abstract methods and properties (as well as a handful of virtuals) defining the basic characteristics of a membership provider. MembershipProvider is prototyped as follows:

public abstract class MembershipProvider : ProviderBase
{
    // Abstract properties
    public abstract bool EnablePasswordRetrieval { get; }
    public abstract bool EnablePasswordReset { get; }
    public abstract bool RequiresQuestionAndAnswer { get; }
    public abstract string ApplicationName { get; set; }
    public abstract int MaxInvalidPasswordAttempts { get; }
    public abstract int PasswordAttemptWindow { get; }
    public abstract bool RequiresUniqueEmail { get; }
    public abstract MembershipPasswordFormat PasswordFormat { get; }
    public abstract int MinRequiredPasswordLength { get; }
    public abstract int MinRequiredNonAlphanumericCharacters { get; }
    public abstract string PasswordStrengthRegularExpression { get; }

    // Abstract methods
    public abstract MembershipUser CreateUser (string username, 
        string password, string email, string passwordQuestion, 
        string passwordAnswer, bool isApproved, object providerUserKey,
        out MembershipCreateStatus status);

    public abstract bool ChangePasswordQuestionAndAnswer
        (string username, string password,
        string newPasswordQuestion, string newPasswordAnswer);

    public abstract string GetPassword (string username,
      string answer);

    public abstract bool ChangePassword (string username,
        string oldPassword, string newPassword);

    public abstract string ResetPassword (string username,
        string answer);

    public abstract void UpdateUser (MembershipUser user);

    public abstract bool ValidateUser (string username,
        string password);

    public abstract bool UnlockUser (string userName);

    public abstract MembershipUser GetUser (object providerUserKey,
        bool userIsOnline);

    public abstract MembershipUser GetUser (string username,
        bool userIsOnline);

    public abstract string GetUserNameByEmail (string email);

    public abstract bool DeleteUser (string username,
        bool deleteAllRelatedData);

    public abstract MembershipUserCollection GetAllUsers
        (int pageIndex, int pageSize, out int totalRecords);

    public abstract int GetNumberOfUsersOnline ();

    public abstract MembershipUserCollection FindUsersByName
        (string usernameToMatch, int pageIndex, int pageSize,
        out int totalRecords);

    public abstract MembershipUserCollection FindUsersByEmail
        (string emailToMatch, int pageIndex, int pageSize,
        out int totalRecords);

    // Virtual methods
    protected virtual byte[] EncryptPassword (byte[] password);
    protected virtual byte[] DecryptPassword (byte[] encodedPassword);
    protected virtual void OnValidatingPassword
        (ValidatePasswordEventArgs e);

    // Events
    public event MembershipValidatePasswordEventHandler
        ValidatingPassword;
}

The following table describes MembershipProvider's methods and properties and provides helpful notes regarding their implementation:

Method or Property Description
EnablePasswordRetrieval Indicates whether passwords can be retrieved using the provider's GetPassword method. This property is read-only.
EnablePasswordReset Indicates whether passwords can be reset using the provider's ResetPassword method. This property is read-only.
RequiresQuestionAndAnswer Indicates whether a password answer must be supplied when calling the provider's GetPassword and ResetPassword methods. This property is read-only.
ApplicationName The name of the application using the membership provider. ApplicationName is used to scope membership data so that applications can choose whether to share membership data with other applications. This property can be read and written.
MaxInvalidPasswordAttempts Works in conjunction with PasswordAttemptWindow to provide a safeguard against password guessing. If the number of consecutive invalid passwords or password questions ("invalid attempts") submitted to the provider for a given user reaches MaxInvalidPasswordAttempts within the number of minutes specified by PasswordAttemptWindow, the user is locked out of the system. The user remains locked out until the provider's UnlockUser method is called to remove the lock.

The count of consecutive invalid attempts is incremented when an invalid password or password answer is submitted to the provider's ValidateUser, ChangePassword, ChangePasswordQuestionAndAnswer, GetPassword, and ResetPassword methods.

If a valid password or password answer is supplied before the MaxInvalidPasswordAttempts is reached, the count of consecutive invalid attempts is reset to zero. If the RequiresQuestionAndAnswer property is false, invalid password answer attempts are not tracked.

This property is read-only.

PasswordAttemptWindow For a description, see MaxInvalidPasswordAttempts. This property is read-only.
RequiresUniqueEmail Indicates whether each registered user must have a unique e-mail address. This property is read-only.
PasswordFormat Indicates what format that passwords are stored in: clear (plaintext), encrypted, or hashed. Clear and encrypted passwords can be retrieved; hashed passwords cannot. This property is read-only.
MinRequiredPasswordLength The minimum number of characters required in a password. This property is read-only.
MinRequiredNonAlphanumericCharacters The minimum number of non-alphanumeric characters required in a password. This property is read-only.
PasswordStrengthRegularExpression A regular expression specifying a pattern to which passwords must conform. This property is read-only.
CreateUser Takes, as input, a user name, password, e-mail address, and other information and adds a new user to the membership data source. CreateUser returns a MembershipUser object representing the newly created user. It also accepts an out parameter (in Visual Basic, ByRef) that returns a MembershipCreateStatus value indicating whether the user was successfully created or, if the user was not created, the reason why. If the user was not created, CreateUser returns null.

Before creating a new user, CreateUser calls the provider's virtual OnValidatingPassword method to validate the supplied password. It then creates the user or cancels the action based on the outcome of the call.

ChangePasswordQuestionAndAnswer Takes, as input, a user name, password, password question, and password answer and updates the password question and answer in the data source if the user name and password are valid. This method returns true if the password question and answer are successfully updated. Otherwise, it returns false.

ChangePasswordQuestionAndAnswer returns false if either the user name or password is invalid.

GetPassword Takes, as input, a user name and a password answer and returns that user's password. If the user name is not valid, GetPassword throws a ProviderException.

Before retrieving a password, GetPassword verifies that EnablePasswordRetrieval is true. If EnablePasswordRetrieval is false, GetPassword throws a NotSupportedException. If EnablePasswordRetrieval is true but the password format is hashed, GetPassword throws a ProviderException since hashed passwords cannot, by definition, be retrieved. A membership provider should also throw a ProviderException from Initialize if EnablePasswordRetrieval is true but the password format is hashed.

GetPassword also checks the value of the RequiresQuestionAndAnswer property before retrieving a password. If RequiresQuestionAndAnswer is true, GetPassword compares the supplied password answer to the stored password answer and throws a MembershipPasswordException if the two don't match. GetPassword also throws a MembershipPasswordException if the user whose password is being retrieved is currently locked out.

ChangePassword Takes, as input, a user name, a password (the user's current password), and a new password and updates the password in the membership data source. ChangePassword returns true if the password was updated successfully. Otherwise, it returns false.

Before changing a password, ChangePassword calls the provider's virtual OnValidatingPassword method to validate the new password. It then changes the password or cancels the action based on the outcome of the call.

If the user name, password, new password, or password answer is not valid, ChangePassword does not throw an exception; it simply returns false.

Following a successful password change, ChangePassword updates the user's LastPasswordChangedDate.

ResetPassword Takes, as input, a user name and a password answer and replaces the user's current password with a new, random password. ResetPassword then returns the new password. A convenient mechanism for generating a random password is the Membership.GeneratePassword method.

If the user name is not valid, ResetPassword throws a ProviderException. ResetPassword also checks the value of the RequiresQuestionAndAnswer property before resetting a password. If RequiresQuestionAndAnswer is true, ResetPassword compares the supplied password answer to the stored password answer and throws a MembershipPasswordException if the two don't match.

Before resetting a password, ResetPassword verifies that EnablePasswordReset is true. If EnablePasswordReset is false, ResetPassword throws a NotSupportedException. If the user whose password is being changed is currently locked out, ResetPassword throws a MembershipPasswordException.

Before resetting a password, ResetPassword calls the provider's virtual OnValidatingPassword method to validate the new password. It then resets the password or cancels the action based on the outcome of the call. If the new password is invalid, ResetPassword throws a ProviderException.

Following a successful password reset, ResetPassword updates the user's LastPasswordChangedDate.

UpdateUser Takes, as input, a MembershipUser object representing a registered user and updates the information stored for that user in the membership data source. If any of the input submitted in the MembershipUser object is not valid, UpdateUser throws a ProviderException.

Note that UpdateUser is not obligated to allow all the data that can be encapsulated in a MembershipUser object to be updated in the data source.

ValidateUser Takes, as input, a user name and a password and verifies that they are valid-that is, that the membership data source contains a matching user name and password. ValidateUser returns true if the user name and password are valid, if the user is approved (that is, if MembershipUser.IsApproved is true), and if the user isn't currently locked out. Otherwise, it returns false.

Following a successful validation, ValidateUser updates the user's LastLoginDate and fires an AuditMembershipAuthenticationSuccess Web event. Following a failed validation, it fires an

AuditMembershipAuthenticationFailure Web event.

UnlockUser Unlocks (that is, restores login privileges for) the specified user. UnlockUser returns true if the user is successfully unlocked. Otherwise, it returns false. If the user is already unlocked, UnlockUser simply returns true.
GetUser Takes, as input, a user name or user ID (the method is overloaded) and a Boolean value indicating whether to update the user's LastActivityDate to show that the user is currently online. GetUser returns a MembershipUser object representing the specified user. If the user name or user ID is invalid (that is, if it doesn't represent a registered user) GetUser returns null (Nothing in Visual Basic).
GetUserNameByEmail Takes, as input, an e-mail address and returns the first registered user name whose e-mail address matches the one supplied.

If it doesn't find a user with a matching e-mail address, GetUserNameByEmail returns an empty string.

DeleteUser Takes, as input, a user name and deletes that user from the membership data source. DeleteUser returns true if the user was successfully deleted. Otherwise, it returns false.

DeleteUser takes a third parameter-a Boolean named deleteAllRelatedData-that specifies whether related data for that user should be deleted also. If deleteAllRelatedData is true, DeleteUser should delete role data, profile data, and all other data associated with that user.

GetAllUsers Returns a MembershipUserCollection containing MembershipUser objects representing all registered users. If there are no registered users, GetAllUsers returns an empty MembershipUserCollection

The results returned by GetAllUsers are constrained by the pageIndex and pageSize input parameters. pageSize specifies the maximum number of MembershipUser objects to return. pageIndex identifies which page of results to return. Page indexes are 0-based.

GetAllUsers also takes an out parameter (in Visual Basic, ByRef) named totalRecords that, on return, holds a count of all registered users.

GetNumberOfUsersOnline Returns a count of users that are currently online-that is, whose LastActivityDate is greater than the current date and time minus the value of the membership service's UserIsOnlineTimeWindow property, which can be read from Membership.UserIsOnlineTimeWindow. UserIsOnlineTimeWindow specifies a time in minutes and is set using the <membership> element's userIsOnlineTimeWindow attribute.
FindUsersByName Returns a MembershipUserCollection containing MembershipUser objects representing users whose user names match the usernameToMatch input parameter. Wildcard syntax is data source-dependent. MembershipUser objects in the MembershipUserCollection are sorted by user name. If FindUsersByName finds no matching users, it returns an empty MembershipUserCollection.

For an explanation of the pageIndex, pageSize, and totalRecords parameters, see the GetAllUsers method.

FindUsersByEmail Returns a MembershipUserCollection containing MembershipUser objects representing users whose e-mail addresses match the emailToMatch input parameter. Wildcard syntax is data source-dependent. MembershipUser objects in the MembershipUserCollection are sorted by e-mail address. If FindUsersByEmail finds no matching users, it returns an empty MembershipUserCollection.

For an explanation of the pageIndex, pageSize, and totalRecords parameters, see the GetAllUsers method.

EncryptPassword Takes, as input, a byte array containing a plaintext password and returns a byte array containing the password in encrypted form. The default implementation in MembershipProvider encrypts the password using <machineKey>'s decryptionKey, but throws an exception if the decryption key is autogenerated.

Override only if you want to customize the encryption process. Do not call the base class's EncryptPassword method if you override this method.

DecryptPassword Takes, as input, a byte array containing an encrypted password and returns a byte array containing the password in plaintext form. The default implementation in MembershipProvider decrypts the password using <machineKey>'s decryptionKey, but throws an exception if the decryption key is autogenerated.

Override only if you want to customize the decryption process. Do not call the base class's DecryptPassword method if you override this method.

OnValidatingPassword Virtual method called when a password is created. The default implementation in MembershipProvider fires a ValidatingPassword event, so be sure to call the base class's OnValidatingPassword method if you override this method. The ValidatingPassword event allows applications to apply additional tests to passwords by registering event handlers.

A custom provider's CreateUser, ChangePassword, and ResetPassword methods (in short, all methods that record new passwords) should call this method.

Your job in implementing a custom membership provider in a derived class is to override and provide implementations of MembershipProvider's abstract members, and optionally to override key virtuals such as Initialize.

Inside the ASP.NET Team
Do you wonder why MembershipProvider's EncryptPassword and DecryptPassword methods throw exceptions if <machineKey>'s decryption key (which also happens to be an encryption key) is autogenerated? Here's how one member of the ASP.NET team explained it:

"Back in the alpha we kept running across developers that worked a little bit on one machine and then picked up their MDB and copied it to another machine. At which point—surprise!—none of the passwords could be decrypted any more. So we decided to disallow autogenerated keys when using encrypted passwords. The reality is that autogenerated keys are really fragile. It's just way too easy to get yourself in a situation where these keys change. And once that happens, you are left with a useless membership database."

Scoping of Membership Data

All membership providers inherit from MembershipProvider a property named ApplicationName whose purpose it to scope the data managed by the provider. Applications that specify the same ApplicationName when configuring the membership service share membership data; applications that specify unique ApplicationNames do not. Membership provider implementations must therefore associate users with application names so operations performed on membership data sources can be scoped accordingly.

As an example, a provider that stores membership data in a SQL database might use a command similar to the following to determine whether a specified user name and password are valid for the application named "Contoso:"

SELECT COUNT (*) FROM Users
WHERE UserName='Jeff' AND Password='imbatman'
AND ApplicationName='Contoso'

The final AND in the WHERE clause ensures that other applications containing identical user names and passwords don't produce false positives in the "Contoso" application.

Strong Password Policies

A full-featured membership provider supports strong password policies. Before creating a password, a membership provider should verify that the password contains at least the number of characters specified by the MinRequiredPasswordLength property, at least the number of non-alphanumeric characters specified by MinRequiredNonAlphaNumericCharacters, and, if PasswordStrengthRegularExpression is neither null nor empty, that the password conforms to the pattern specified by the regular expression stored in that property. A membership consumer can then enact strong password policies in either of two ways:

  • Set the provider's PasswordStrengthRegularExpression property to null and use MinRequiredPasswordLength and MinRequiredNonAlphanumericCharacters to specify minimum character counts
  • Set the provider's MinRequiredPasswordLength property to 1 and MinRequiredNonAlphanumericCharacters to 0 and use PasswordStrengthRegularExpression to specify a regular expression defining acceptable password formats

Password-validation logic should be applied in all provider methods that create or accept new passwords, including CreateUser, ChangePassword, and ResetPassword. This logic is not automatically supplied by MembershipProvider.

Account Locking

A full-featured membership provider also supports the locking out of users after a consecutive number of failed login attempts within a specified time period. A consumer uses the provider's MaxInvalidPasswordAttempts and PasswordAttemptsWindow properties to configure this feature. Once locked out, a user may not log in again until his or her account is unlocked. MembershipProvider defines an Unlock method for unlocking locked-out users, and the System.Web.Security.MembershipUser class, which represents individual users managed by the membership service, includes an IsLockedOut property that indicates whether the corresponding user is currently locked out of the system.

ReadOnlyXmlMembershipProvider

The source code below is for a membership provider named ReadOnlyXmlMembershipProvider that demonstrates the minimum functionality required of a membership provider-the provider equivalent of "hello, world." Despite its simplicity, ReadOnlyXmlMembershipProvider is capable of supporting applications that authenticate users using Login controls or direct calls to Membership.ValidateUser. It also provides data regarding membership users to applications that request it using Membership methods such as GetUser and GetAllUsers. It does not support Membership methods such as CreateUser and ChangePassword that modify the data source, hence the "ReadOnly" in the class name. ReadOnlyXmlMembershipProvider methods that write to the data source throw NotSupportedExceptions if called.

ReadOnlyXmlMembershipProvider

using System;
using System.Xml;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration.Provider;
using System.Web.Security;
using System.Web.Hosting;
using System.Web.Management;
using System.Security.Permissions;
using System.Web;

public class ReadOnlyXmlMembershipProvider : MembershipProvider
{
    private Dictionary<string, MembershipUser> _Users;
    private string _XmlFileName;

    // MembershipProvider Properties
    public override string ApplicationName
    {
        get { throw new NotSupportedException(); }
        set { throw new NotSupportedException(); }
    }
    
    public override bool EnablePasswordRetrieval
    {
        get { return false; }
    }

    public override bool EnablePasswordReset
    {
        get { return false; }
    }

    public override int MaxInvalidPasswordAttempts
    {
        get { throw new NotSupportedException(); }
    }

    public override int MinRequiredNonAlphanumericCharacters
    {
        get { throw new NotSupportedException(); }
    }

    public override int MinRequiredPasswordLength
    {
        get { throw new NotSupportedException(); }
    }

    public override int PasswordAttemptWindow
    {
        get { throw new NotSupportedException(); }
    }

    public override MembershipPasswordFormat PasswordFormat
    {
        get { throw new NotSupportedException(); }
    }

    public override string PasswordStrengthRegularExpression
    {
        get { throw new NotSupportedException(); }
    }

    public override bool RequiresQuestionAndAnswer
    {
        get { throw new NotSupportedException(); }
    }

    public override bool RequiresUniqueEmail
    {
        get { throw new NotSupportedException(); }
    }

    // MembershipProvider Methods
    public override void Initialize (string name,
        NameValueCollection config)
    {
        // Verify that config isn't null
        if (config == null)
            throw new ArgumentNullException ("config");

        // Assign the provider a default name if it doesn't have one
        if (String.IsNullOrEmpty (name))
            name = "ReadOnlyXmlMembershipProvider";

        // Add a default "description" attribute to config if the
        // attribute doesn't exist or is empty
        if (string.IsNullOrEmpty (config["description"])) {
            config.Remove ("description");
            config.Add ("description",
                "Read-only XML membership provider");
        }

        // Call the base class's Initialize method
        base.Initialize(name, config);

        // Initialize _XmlFileName and make sure the path
        // is app-relative
        string path = config["xmlFileName"];

        if (String.IsNullOrEmpty (path))
            path = "~/App_Data/Users.xml";

        if (!VirtualPathUtility.IsAppRelative(path))
            throw new ArgumentException
                ("xmlFileName must be app-relative");

        string fullyQualifiedPath = VirtualPathUtility.Combine
            (VirtualPathUtility.AppendTrailingSlash
            (HttpRuntime.AppDomainAppVirtualPath), path);

        _XmlFileName = HostingEnvironment.MapPath(fullyQualifiedPath);
        config.Remove ("xmlFileName");

        // Make sure we have permission to read the XML data source and
        // throw an exception if we don't
        FileIOPermission permission =
            new FileIOPermission(FileIOPermissionAccess.Read,
            _XmlFileName);
        permission.Demand();
                
        // Throw an exception if unrecognized attributes remain
        if (config.Count > 0) {
            string attr = config.GetKey (0);
            if (!String.IsNullOrEmpty (attr))
                throw new ProviderException
                    ("Unrecognized attribute: " + attr);
        }
    }

    public override bool ValidateUser(string username, string password)
    {
        // Validate input parameters
        if (String.IsNullOrEmpty(username) ||
            String.IsNullOrEmpty(password))
            return false;
        
        try
        {
            // Make sure the data source has been loaded
            ReadMembershipDataStore();

            // Validate the user name and password
            MembershipUser user;
            if (_Users.TryGetValue (username, out user))
            {
                if (user.Comment == password) // Case-sensitive
                {
                    // NOTE: A read/write membership provider
                    // would update the user's LastLoginDate here.
                    // A fully featured provider would also fire
                    // an AuditMembershipAuthenticationSuccess
                    // Web event
                    return true;
                }
            }

            // NOTE: A fully featured membership provider would
            // fire an AuditMembershipAuthenticationFailure
            // Web event here
            return false;
        }
        catch (Exception)
        {
            return false;
        }
    }

    public override MembershipUser GetUser(string username,
        bool userIsOnline)
    {
        // Note: This implementation ignores userIsOnline

        // Validate input parameters
        if (String.IsNullOrEmpty(username))
            return null;

        // Make sure the data source has been loaded
        ReadMembershipDataStore();

        // Retrieve the user from the data source
        MembershipUser user;
        if (_Users.TryGetValue (username, out user))
            return user;

        return null;
    }

    public override MembershipUserCollection GetAllUsers(int pageIndex,
        int pageSize, out int totalRecords)
    {
        // Note: This implementation ignores pageIndex and pageSize,
        // and it doesn't sort the MembershipUser objects returned

        // Make sure the data source has been loaded
        ReadMembershipDataStore();

        MembershipUserCollection users =
            new MembershipUserCollection();

        foreach (KeyValuePair<string, MembershipUser> pair in _Users)
            users.Add(pair.Value);

        totalRecords = users.Count;
        return users;
    }

    public override int GetNumberOfUsersOnline()
    {
        throw new NotSupportedException();
    }

    public override bool ChangePassword(string username,
        string oldPassword, string newPassword)
    {
        throw new NotSupportedException();
    }

    public override bool
        ChangePasswordQuestionAndAnswer(string username,
        string password, string newPasswordQuestion,
        string newPasswordAnswer)
    {
        throw new NotSupportedException();
    }

    public override MembershipUser CreateUser(string username,
        string password, string email, string passwordQuestion,
        string passwordAnswer, bool isApproved, object providerUserKey,
        out MembershipCreateStatus status)
    {
        throw new NotSupportedException();
    }

    public override bool DeleteUser(string username,
        bool deleteAllRelatedData)
    {
        throw new NotSupportedException();
    }

    public override MembershipUserCollection
        FindUsersByEmail(string emailToMatch, int pageIndex,
        int pageSize, out int totalRecords)
    {
        throw new NotSupportedException();
    }

    public override MembershipUserCollection
        FindUsersByName(string usernameToMatch, int pageIndex,
        int pageSize, out int totalRecords)
    {
        throw new NotSupportedException();
    }

    public override string GetPassword(string username, string answer)
    {
        throw new NotSupportedException();
    }

    public override MembershipUser GetUser(object providerUserKey,
        bool userIsOnline)
    {
        throw new NotSupportedException();
    }

    public override string GetUserNameByEmail(string email)
    {
        throw new NotSupportedException();
    }

    public override string ResetPassword(string username,
        string answer)
    {
        throw new NotSupportedException();
    }

    public override bool UnlockUser(string userName)
    {
        throw new NotSupportedException();
    }

    public override void UpdateUser(MembershipUser user)
    {
        throw new NotSupportedException();
    }

    // Helper method
    private void ReadMembershipDataStore()
    {
        lock (this)
        {
            if (_Users == null)
            {
                _Users = new Dictionary<string, MembershipUser>
                   (16, StringComparer.InvariantCultureIgnoreCase);
                XmlDocument doc = new XmlDocument();
                doc.Load(_XmlFileName);
                XmlNodeList nodes = doc.GetElementsByTagName("User");

                foreach (XmlNode node in nodes)
                {
                    MembershipUser user = new MembershipUser(
                        Name,                       // Provider name
                        node["UserName"].InnerText, // Username
                        null,                       // providerUserKey
                        node["EMail"].InnerText,    // Email
                        String.Empty,               // passwordQuestion
                        node["Password"].InnerText, // Comment
                        true,                       // isApproved
                        false,                      // isLockedOut
                        DateTime.Now,               // creationDate
                        DateTime.Now,               // lastLoginDate
                        DateTime.Now,               // lastActivityDate
                        DateTime.Now, // lastPasswordChangedDate
                        new DateTime(1980, 1, 1)    // lastLockoutDate
                    );

                    _Users.Add(user.UserName, user);
                }
            }
        }
    }
}

ReadOnlyXmlMembershipProvider uses an XML file with a schema matching that of the below as its data source. Each <User> element defines one membership user. To avoid redundant file I/O and XML parsing, the provider reads the XML file once and stores the data in a dictionary of MembershipUser objects. Each object in the dictionary is keyed with a user name, making lookups fast and easy.

Sample ReadOnlyXmlMembershipProvider Data Source

<Users>
  <User>
    <UserName>Bob</UserName>
    <Password>contoso!</Password>
    <EMail>bob@contoso.com</EMail>
  </User>
  <User>
    <UserName>Alice</UserName>
    <Password>contoso!</Password>
    <EMail>alice@contoso.com</EMail>
  </User>
</Users>

ReadOnlyXmlMembershipProvider supports one custom configuration attribute: xmlFileName. The provider's Initialize method initializes a private field named _XmlFileName with the attribute value and defaults to ~/App_Data/Users.xml if the attribute isn't present. The Web.config file below registers ReadOnlyXmlMembershipProvider, makes it the default membership provider, and points it to MembershipUsers.xml (located in the application root) as the data source. The type name specified in the <add> element assumes that the provider is deployed in an assembly named CustomProviders.

Web.config file making ReadOnlyXmlMembershipProvider the default membership provider

<configuration>
  <system.web>
    <membership defaultProvider="AspNetReadOnlyXmlMembershipProvider">
      <providers>
        <add name="AspNetReadOnlyXmlMembershipProvider"
          type="ReadOnlyXmlMembershipProvider, CustomProviders"
          description="Read-only XML membership provider"
          xmlFileName="~/App_Data/MembershipUsers.xml"
        />
      </providers>
    </membership>
  </system.web>
</configuration>

As you peruse ReadOnlyXmlMembershipProvider's source code, here are a few key points to keep in mind regarding its implementation:

  • For simplicity, ReadOnlyXmlMembershipProvider doesn't support encrypted or hashed passwords. Passwords are stored in plaintext, and they're stored in the Comment properties of the corresponding MembershipUser objects since the MembershipUser class, by design, lacks a Password property. In practice, MembershipUser objects should never store passwords. Passwords should stay in the data source, and they should be stored in encrypted or hashed form in the absence of a compelling reason to do otherwise. (In fact, it's quite acceptable for membership providers to not support plaintext password storage as long as that fact is documented.)
  • ReadOnlyXmlMembershipProvider doesn't read the XML data source in Initialize; rather, it loads it on demand, the first time the data is needed. This is done for a pragmatic reason. The very act of creating MembershipUser objects in Initialize would cause Initialize to be called again, resulting in an infinite loop and an eventual stack overflow. As stated in Provider Initialization in Introduction to the Provider Model, a provider's Initialize method must avoid making calls into the service that the provider serves because doing so may cause deadly reentrancies.
  • For simplicity, ReadOnlyXmlMembershipProvider doesn't scope membership data using the ApplicationName property. Instead, it assumes that different applications will target different membership data sources by specifying different XML file names.
  • ReadOnlyXmlMembershipProvider's GetAllUsers method doesn't honor the pageIndex and PageSize parameters, nor does it sort the MembershipUser objects that it returns by user name.
  • ReadOnlyXmlMembershipProvider contains minimal thread synchronization code because most of its work involves reading, not writing. It does lock when reading the membership data source to ensure that two threads won't try to initialize the in-memory representation of that source (a Dictionary object) at the same time.
  • ReadOnlyXmlMembershipProvider.Initialize calls Demand on a FileIOPermission object to verify that it can read the XML data source. It delays making the call until after processing the xmlFileName configuration attribute so it knows the data source's file name.

ReadOnlyXmlMembershipProvider is a good starting point for understanding membership providers, but a full-featured provider must implement methods that write to the data source as well as methods that read from them. A full-featured provider must also support non-cleartext password storage and scoping by ApplicationName.

Click here to continue on to part 2, Role Providers

© Microsoft Corporation. All rights reserved.