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 mustoverride methods and properties (as well as a handful of overridable) defining the basic characteristics of a membership provider. MembershipProvider is prototyped as follows:

Public MustInherit Class MembershipProvider
   Inherits ProviderBase
  
  'MustOverride Properties
  Public MustOverride ReadOnly Property EnablePasswordRetrieval As Boolean
  Public MustOverride ReadOnly Property EnablePasswordReset As Boolean
  Public MustOverride ReadOnly Property _
RequiresQuestionAndAnswer As   Boolean
  Public MustOverride Property ApplicationName As String
  Public MustOverride ReadOnly Property _
MaxInvalidPasswordAttempts As Integer
  Public MustOverride ReadOnly Property PasswordAttemptWindow As Integer
  Public MustOverride ReadOnly Property RequiresUniqueEmail As Boolean
  Public MustOverride ReadOnly Property _
PasswordFormat As MembershipPasswordFormat
  Public MustOverride ReadOnly Property _
MinRequiredPasswordLength As Integer
  Public MustOverride ReadOnly Property _  
       MinRequiredNonAlphanumericCharacters As Integer
  Public MustOverride ReadOnly Property _
         PasswordStrengthRegularExpression As String

  'MustOverride Methods
  Public MustOverride Function CreateUser ( _
   username As String, _
   password As String, _
   email As String, _
   passwordQuestion As String, _
   passwordAnswer As String, _
   isApproved As Boolean, _
   providerUserKey As Object, _
   <OutAttribute> ByRef status As MembershipCreateStatus _
  ) As MembershipUser

  Public MustOverride Function ChangePasswordQuestionAndAnswer ( _
   username As String, _
   password As String, _
   newPasswordQuestion As String, _
   newPasswordAnswer As String _
  ) As Boolean

  Public MustOverride Function GetPassword ( _
   username As String, _
   answer As String _
  ) As String

  Public MustOverride Function ChangePassword ( _
   username As String, _
   oldPassword As String, _
   newPassword As String _
  ) As Boolean

  Public MustOverride Function ResetPassword ( _
   username As String, _
   answer As String _
  ) As String

  Public MustOverride Sub UpdateUser ( _
   user As MembershipUser _
  )

  Public MustOverride Function ValidateUser ( _
   username As String, _
   password As String _
  ) As Boolean

  Public MustOverride Function UnlockUser ( _
   userName As String _
  ) As Boolean

  Public MustOverride Function GetUser ( _
   providerUserKey As Object, _
   userIsOnline As Boolean _
  ) As MembershipUser

  Public MustOverride Function GetUser ( _
   username As String, _
   userIsOnline As Boolean _
  ) As MembershipUser

  Public MustOverride Function GetUserNameByEmail ( _
   email As String _
  ) As String

  Public MustOverride Function DeleteUser ( _
   username As String, _
   deleteAllRelatedData As Boolean _
  ) As Boolean

  Public MustOverride Function GetAllUsers ( _
   pageIndex As Integer, _
   pageSize As Integer, _
   <OutAttribute> ByRef totalRecords As Integer _
  ) As MembershipUserCollection

  Public MustOverride Function GetNumberOfUsersOnline As Integer

  Public MustOverride Function FindUsersByName ( _
   usernameToMatch As String, _
   pageIndex As Integer, _
   pageSize As Integer, _
   <OutAttribute> ByRef totalRecords As Integer _
  ) As MembershipUserCollection

  Public MustOverride Function FindUsersByEmail ( _
   emailToMatch As String, _
   pageIndex As Integer, _
   pageSize As Integer, _
   <OutAttribute> ByRef totalRecords As Integer _
  ) As MembershipUserCollection

  'Overrideable Methods
  Protected Overridable Function EncryptPassword ( _
   password As Byte() _
  ) As Byte()

  Protected Overridable Function DecryptPassword ( _
   encodedPassword As Byte() _
  ) As Byte()

  Protected Overridable Sub OnValidatingPassword ( _
   e As ValidatePasswordEventArgs _
  )

  'Events
  Public Event ValidatingPassword As MembershipValidatePasswordEventHandler

End Class

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 a ByRef parameter that returns a MembershipCreateStatus value indicating whether the user was successfully created or, if the user was not created, then the reason why. If the user was not created, CreateUser returns null.

Before creating a new user, CreateUser calls the provider's overridable 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 overridable 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 overridable 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 a ByRef parameter 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 mustoverride members, and optionally to override key overridable 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 Nothing 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

Imports System
Imports System.Xml
Imports System.Collections.Generic
Imports System.Collections.Specialized
Imports System.Configuration.Provider
Imports System.Web.Security
Imports System.Web.Hosting
Imports System.Web.Management
Imports System.Security.Permissions
Imports System.Web
Public Class ReadOnlyXmlMembershipProvider
   Inherits MembershipProvider

  Private _Users As Dictionary(Of String, MembershipUser)
  Private _XmlFileName As String

  ' MembershipProvider Properties
  Public Overrides Property ApplicationName() As String
        Get
            Throw New NotSupportedException
        End Get
        Set(ByVal value As String)
            Throw New NotSupportedException
        End Set
  End Property

  Public Overrides ReadOnly Property EnablePasswordRetrieval() As Boolean
        Get
            Return False
        End Get
  End Property

  Public Overrides ReadOnly Property EnablePasswordReset() As Boolean
        Get
            Return False
        End Get
  End Property

  Public Overrides ReadOnly Property _
MaxInvalidPasswordAttempts() As Integer
        Get
            Throw New NotSupportedException
        End Get
  End Property

  Public Overrides ReadOnly Property _
         MinRequiredNonAlphanumericCharacters() As Integer
        Get
            Throw New NotSupportedException
        End Get
  End Property

  Public Overrides ReadOnly Property _
         MinRequiredPasswordLength() As Integer
        Get
            Throw New NotSupportedException
        End Get
  End Property

  Public Overrides ReadOnly Property PasswordAttemptWindow() As Integer
        Get
            Throw New NotSupportedException
        End Get
  End Property

  Public Overrides ReadOnly Property PasswordFormat( _
) As MembershipPasswordFormat
        Get
            Throw New NotSupportedException
        End Get
  End Property

  Public Overrides ReadOnly Property _
   PasswordStrengthRegularExpression() As String
        Get
            Throw New NotSupportedException
        End Get
  End Property

  Public Overrides ReadOnly Property _
   RequiresQuestionAndAnswer() As Boolean
        Get
            Throw New NotSupportedException
        End Get
  End Property

  Public Overrides ReadOnly Property RequiresUniqueEmail() As Boolean
        Get
            Throw New NotSupportedException
        End Get
  End Property

  ' MembershipProvider Methods
  Public Overrides Sub Initialize(ByVal name As String, _
ByVal config As NameValueCollection)
       
 ' Verify that config isn't null
        If (config Is Nothing) Then
            Throw New ArgumentNullException("config")
        End If
        If String.IsNullOrEmpty(name) Then
            name = "ReadOnlyXmlMembershipProvider"
        End If
        ' Add a default "description" attribute to config if the
        ' attribute doesn't exist or is empty
        If String.IsNullOrEmpty(config("description")) Then
            config.Remove("description")
            config.Add("description", "Read-only XML membership provider")
        End If
        ' Call the base class's Initialize method
        MyBase.Initialize(name, config)
        ' Initialize _XmlFileName and make sure the path
        ' is app-relative
        Dim path As String = config("xmlFileName")
        If String.IsNullOrEmpty(path) Then
            path = "~/App_Data/Users.xml"
        End If
        If Not VirtualPathUtility.IsAppRelative(path) Then
            Throw New _
ArgumentException("xmlFileName must be app-relative")
        End If
        Dim fullyQualifiedPath As String = _
          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
Dim permission As FileIOPermission = New _ FileIOPermission(FileIOPermissionAccess.Read, _XmlFileName)
        permission.Demand()
        ' Throw an exception if unrecognized attributes remain
        If (config.Count > 0) Then
            Dim attr As String = config.GetKey(0)
            If Not String.IsNullOrEmpty(attr) Then
                Throw New _
ProviderException(("Unrecognized attribute: " + attr))
            End If
        End If
  End Sub

  Public Overrides Function ValidateUser(ByVal username As String, _
       ByVal password As String) As Boolean
        ' Validate input parameters
     If (String.IsNullOrEmpty(username) OrElse _       
         String.IsNullOrEmpty(password)) Then
            Return False
        End If
        Try
            ' Make sure the data source has been loaded
            ReadMembershipDataStore()
            ' Validate the user name and password
            Dim user As MembershipUser = Nothing
            If _Users.TryGetValue(username, user) Then
                If (user.Comment = password) Then
                    ' 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
                End If
            End If
            ' NOTE: A fully featured membership provider would
            ' fire an AuditMembershipAuthenticationFailure
            ' Web event here
            Return False
        Catch e As Exception
            Return False
        End Try
  End Function

  Public Overloads Overrides Function GetUser( _
ByVal username As String, _
         ByVal userIsOnline As Boolean _
) As MembershipUser
        
        ' Note: This implementation ignores userIsOnline
        ' Validate input parameters
        If String.IsNullOrEmpty(username) Then
            Return Nothing
        End If
        ' Make sure the data source has been loaded
        ReadMembershipDataStore()
        ' Retrieve the user from the data source
        Dim user As MembershipUser
        If _Users.TryGetValue(username, user) Then
            Return user
        End If
        Return Nothing
  End Function

  Public Overrides Function GetAllUsers( _
ByVal pageIndex As Integer, _
ByVal pageSize As Integer, _
         ByRef totalRecords As Integer _
) As MembershipUserCollection
        
        ' 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()
  Dim users As MembershipUserCollection = New _   
   MembershipUserCollection
        For Each pair As KeyValuePair(Of String, MembershipUser) In _Users
            users.Add(pair.Value)
        Next
        totalRecords = users.Count
        Return users
  End Function

  Public Overrides Function GetNumberOfUsersOnline() As Integer
        Throw New NotSupportedException
  End Function

  Public Overrides Function ChangePassword( _
ByVal username As String, _
ByVal oldPassword As String, _
         ByVal newPassword As String _
) As Boolean
        
Throw New NotSupportedException
  End Function

  Public Overrides Function ChangePasswordQuestionAndAnswer( _
ByVal username As String, _
         yVal password As String, ByVal newPasswordQuestion As String, _
         ByVal newPasswordAnswer As String _
) As Boolean
        
Throw New NotSupportedException
  End Function

  Public Overrides Function CreateUser(ByVal username As String, _
ByVal password As String, _
         ByVal email As String, _
ByVal passwordQuestion As String, _
         ByVal passwordAnswer As String,_
ByVal isApproved As Boolean, _
         ByVal providerUserKey As Object, _
         ByRef status As MembershipCreateStatus _
) As MembershipUser
        
Throw New NotSupportedException
  End Function

  Public Overrides Function DeleteUser( _
ByVal username As String, _
         ByVal deleteAllRelatedData As Boolean _
) As Boolean
        
Throw New NotSupportedException
  End Function

  Public Overrides Function FindUsersByEmail( _
ByVal emailToMatch As String, _
         ByVal pageIndex As Integer, 
ByVal pageSize As Integer, _
         ByRef totalRecords As Integer _
) As MembershipUserCollection
        
Throw New NotSupportedException
  End Function

  Public Overrides Function FindUsersByName( _
ByVal usernameToMatch As String, _
         ByVal pageIndex As Integer, 
ByVal pageSize As Integer, _
         ByRef totalRecords As Integer _
) As MembershipUserCollection
        
Throw New NotSupportedException
  End Function

  Public Overrides Function GetPassword( _
ByVal username As String, _
ByVal answer As String _
) As String
        
Throw New NotSupportedException
  End Function

  Public Overloads Overrides Function GetUser( _
ByVal providerUserKey As Object, _
         ByVal userIsOnline As Boolean _
) As MembershipUser
        
Throw New NotSupportedException
  End Function

  Public Overrides Function GetUserNameByEmail( _
ByVal email As String) As String
        
Throw New NotSupportedException
  End Function

  Public Overrides Function ResetPassword( _
ByVal username As String,   
         ByVal answer As String _
) As String
        
Throw New NotSupportedException
  End Function

  Public Overrides Function UnlockUser( _
ByVal userName As String) As Boolean
        
Throw New NotSupportedException
  End Function

  Public Overrides Sub UpdateUser(ByVal user As MembershipUser)
        Throw New NotSupportedException
  End Sub

  ' Helper method
  Private Sub ReadMembershipDataStore()
        SyncLock Me
            If (_Users Is Nothing) Then
                _Users = New Dictionary(Of String, MembershipUser) _
                       (16, StringComparer.InvariantCultureIgnoreCase)
                Dim doc As XmlDocument = New XmlDocument
                doc.Load(_XmlFileName)
                Dim nodes As XmlNodeList = _
       doc.GetElementsByTagName("User")
                For Each node As XmlNode In nodes
          Dim user As MembershipUser = New MembershipUser( _
Name, _
node("UserName").InnerText, _
                     Nothing, 
node("EMail").InnerText, 
String.Empty, _
                     node("Password").InnerText, 
True, 
False, _
                     DateTime.Now, 
DateTime.Now, 
DateTime.Now, _
                     DateTime.Now, 
New DateTime(1980, 1, 1))
                    
  _Users.Add(user.UserName, user)
                Next
            End If
        End SyncLock
    End Sub
End Class

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.