Forms Authentication in SharePoint Products and Technologies (Part 2): Membership and Role Provider Samples

Summary: Explore details of developing a custom membership and role provider for Microsoft Office SharePoint Server 2007 and Windows SharePoint Services 3.0, including the minimum required interfaces and how to register and debug your custom provider. This article is part 2 of 3. (26 printed pages)

Steve Peschka, Microsoft Corporation

December 2007

Applies to: Microsoft Office SharePoint Server 2007, Windows SharePoint Services 3.0

Contents

  • Developing Custom Membership and Role Providers

  • Applying the XML File for the Data Source

  • Inheriting from the Membership and Role Base Classes

  • Minimum Interfaces Required by Office SharePoint Server and Windows SharePoint Services

  • Registering the Custom Provider

  • Debugging the Custom Provider

  • Writing a Custom Forms Logon Page

  • Using Web Services with a Site Protected by Forms Authentication

  • Additional Resources

Read part 1 and part 3:

Forms Authentication in SharePoint Products and Technologies (Part 1): Introduction

Forms Authentication in SharePoint Products and Technologies (Part 3): Forms Authentication vs. Windows Authentication

Developing Custom Membership and Role Providers

Microsoft Office SharePoint Server 2007 and Windows SharePoint Services 3.0 (in this article series, collectively referred to as SharePoint Products and Technologies) are built upon the ASP.NET 2.0 Framework. As such, support for forms authentication extends not only to the membership and role providers that are included with ASP.NET, Office SharePoint Server, and Windows SharePoint Services, but also to custom membership and role providers. The custom provider is required to inherit only from the ASP.NET membership or role base class respectively, and to implement a limited set of interfaces on those classes.

Note

Addressing this part of the Microsoft .NET Framework extensively is beyond the scope of this article. For more information about the membership and role provider base classes, see Membership Class (System.Web.Security)and RoleProvider Class (System.Web.Security).

Applying the XML File for the Data Source

We now describe how to write a custom provider for users and roles that uses an XML file for the directory information. The format of the XML file looks like the following code.

Important

It is NOT a safe or secure practice to include unencrypted passwords in a clear text file, as displayed in the following code example. We are using it in this case only to simplify the explanation of developing custom providers. This is not an acceptable design for a production application. When storing sensitive information in a configuration file for an application, you should encrypt the sensitive values by using Protected Configuration. For more information, see Encrypting Configuration Information Using Protected Configurationand Securing Membership. If you take the approach used as an example in this article, you are responsible for encrypting and decrypting the data within your provider.

<Users>
  <User name="user1" email="user1@microsoft.com" password="test" created="7/15/2007">
    <Groups>
      <Group>Administrators</Group>
      <Group>Authors</Group>
      <Group>Readers</Group>
    </Groups>
  </User>
  <User name="user2" email="user2@microsoft.com" password="test" created="7/31/2007">
    <Groups>
      <Group>Readers</Group>
    </Groups>
  </User>
  <User name="user3" email="user3@microsoft.com" password="test" created="8/15/2007">
    <Groups>
      <Group>Designers</Group>
      <Group>Readers</Group>
    </Groups>
  </User>
</Users>

Inheriting from the Membership and Role Base Classes

The base class that is used for membership is System.Web.Security.MembershipProvider; if you are writing a custom membership provider you must inherit from this class.

The base class that is used for roles is System.Web.Security.RoleProvider; if you are writing a custom role provider you must inherit from this class.

using System.Web;
using System.Web.Security;
using System.Xml;
using System.Configuration;

namespace Microsoft.IW
{
   public class customUser : MembershipProvider
   {
      // This is not necessary for every provider; we are doing  
      // it for this one provider so that we can ensure our XML 
      // file is set up correctly.
      private XmlDocument xDoc = null;
      
      // Code in here.
   }
   public class customRole : RoleProvider
   {
      // This is not necessary for every provider; we are doing  
      // it for this one provider so that we can ensure our XML 
      // file is set up correctly.
      private XmlDocument xDoc = null;
      
   //Code in here.
   }
}
Imports System.Web
Imports System.Web.Security
Imports System.Xml
Imports System.Configuration

Namespace Microsoft.IW
   Public Class customUser
      Inherits MembershipProvider

      ' This is not necessary for every provider; we are doing  
      ' it for this one provider so that we can ensure our XML 
      ' file is set up correctly.
      Private xDoc As XmlDocument = Nothing

      ' Code in here.
   End Class
   Public Class customRole
      Inherits RoleProvider

      ' This is not necessary for every provider; we are doing  
      ' it for this one provider so that we can ensure our XML 
      ' file is set up correctly.
      Private xDoc As XmlDocument = Nothing

      ' Code in here.
   End Class
End Namespace

Minimum Interfaces Required by Office SharePoint Server and Windows SharePoint Services

Both the MembershipProvider class and the RoleProvider class include several methods and properties. To use the provider, only a subset of those is required by Office SharePoint Server and Windows SharePoint Services.

To use the MembershipProvider, you must implement the following methods:

To use the RoleProvider, you must implement the following methods:

Note

The following section contains several code examples meant to demonstrate only the implementation of the minimum interfaces required to have a custom membership and role provider that is usable with Office SharePoint Server 2007 and Windows SharePoint Services 3.0. They are not meant to demonstrate coding best practices.

In addition to the methods that SharePoint Products and Technologies require, the class contract requires you to implement some additional methods and properties. In those cases, you can either implement the functionality or choose to throw a System.NotSupportedException. For example, the MembershipProvider requires a System.Web.Security.MembershipProvider.ChangePassword(System.String,System.String,System.String) method; if you do not want to support that functionality, your override of that method should resemble the following code.

public override bool ChangePassword(string username, string oldPassword, string newPassword)
{
  throw new NotSupportedException();
}
Public Overrides Function ChangePassword(ByVal username As String, ByVal oldPassword As String, ByVal newPassword As String) As Boolean
  Throw New NotSupportedException()
End Function

As with most custom providers, this example includes special code to work with the data store: in this case, an XML file. A simple helper class was developed to read the file from a hard-coded location on disk and store it in cache. The item was added into cache with a dependency so that if the file on disk changes, such as when a user is added or removed, the XML file is purged from cache. Following is the code for the helper class.

using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Diagnostics;
using System.Web;

namespace Microsoft.IW
{
  internal class Helper
  {
    private const string XML_PATH = 
    "c:\\inetpub\\wwwroot\\userdata\\users.xml";
    private const string XML_CACHE = "xmlUserDocFile";

    public static XmlDocument GetXmlFile()
    {
      // This helper function looks for the XML document 
      // in cache; if it is there,
      // it pulls it out of cache. Otherwise, it loads 
      // it into an XML document
      // and stores it in cache with a file cache
      // dependency, so that if the
      // the XML file changes it will be flushed out 
      // of cache and have to be reread and reloaded.

      XmlDocument xDoc = null;

      try
      {
        // Look for the item in cache.
        if (HttpContext.Current.Cache[XML_CACHE] == null)
        {
          // Create a new document.
          xDoc = new XmlDocument();

          // Load it from disk.
          xDoc.Load(XML_PATH);

          // Save it to cache.
          HttpContext.Current.Cache.Insert(XML_CACHE,
            xDoc, new 
            System.Web.Caching.CacheDependency(XML_PATH));
        }
        else
          xDoc = 
           (XmlDocument)HttpContext.Current.Cache[XML_CACHE];
      }
    catch (Exception ex)
    {
      // Try writing to the event log.
      try
      {
        EventLog.WriteEntry("fbaSharp Custom Provider",
          "Error loading user xml file: " +
          ex.Message, EventLogEntryType.Error);
      }
      catch
      {
        // Ignore.
      }
    }

    return xDoc;
    }
  }
}
Imports System.Xml
Imports System.Diagnostics
Imports System.Web

Namespace Microsoft.IW
Friend Class Helper

Private Const XML_PATH As String = _
"c:\inetpub\wwwroot\userdata\users.xml"
Private Const XML_CACHE As String = "xmlUserDocFile"

Public Shared Function GetXmlFile() As XmlDocument

  ' This helper function looks for the XML document 
  ' in cache; if it is there, it pulls it out of cache.
  ' Otherwise, it loads it into an XML document and
  ' stores it in cache with a file cache dependency, 
  ' so that if the XML file changes it will be
  ' flushed out of cache and have to be reread
  ' and reloaded.

  Dim xDoc As XmlDocument = Nothing

  Try
    ' Look for the item in cache.
    If HttpContext.Current.Cache(XML_CACHE) Is Nothing Then
      ' Create a new document.
      xDoc = New XmlDocument

      ' Load it from disk.
      xDoc.Load(XML_PATH)

      ' Save it to cache.
      HttpContext.Current.Cache.Insert(XML_CACHE, xDoc, _
        New System.Web.Caching.CacheDependency(XML_PATH))
    Else
      xDoc = HttpContext.Current.Cache(XML_CACHE)
    End If
  Catch ex As Exception
    ' Try writing to event log.
    Try
      EventLog.WriteEntry("fbaVB Custom Provider", _
        "Error loading user xml file: " & _
        ex.Message, EventLogEntryType.Error)
    Catch logEx As Exception
      ' Ignore.
    End Try
  End Try
  Return xDoc
End Function
End Class
End Namespace

Functions Required in a Custom Membership Provider

The following sections describe the functions (methods) required in a custom membership provider.

GetUser Method

The GetUser function has two overridden implementations and returns a System.Web.Security.MembershipUser object based on user name and a flag that indicates whether a user is online. As with any class, the author must determine whether and how to use the parameters that are passed to it. For example, in most cases it is unlikely that you would change the return value from this function based only on whether the user is online. Notice that the providerUserKey is useful only if you are using a single Membership database to store users for multiple applications and you need to distinguish in which of those applications the user exists. You can find more detail about this parameter on MSDN in the Membership classes materials that are referenced at the beginning of this article. This parameter is controlled in the applicationName attribute of the add element in the web.config file that is used to define a membership provider.

public override MembershipUser GetUser(object providerUserKey, 
bool userIsOnline)
{
  MembershipUser ret = null;
  XmlNode xNode = null;

  try
  {
    // Get the XML document.
    xDoc = Helper.GetXmlFile();

    // Proceed if it contains something.
    if (xDoc != null)
    {
      // Look for a user with a name that matches.
      xNode = xDoc.SelectSingleNode("/Users/User[@name='" + 
        providerUserKey.ToString() + "']");

      // Determine whether there are any matches.
      if (xNode != null)
      {
        // Create a new membershipusercollection.
        ret = new MembershipUser(Membership.Provider.Name,
        xNode.Attributes["name"].Value.ToString(),
        xNode.Attributes["name"].Value.ToString(),
        xNode.Attributes["email"].Value.ToString(),
        string.Empty, string.Empty, true, false,
        DateTime.Parse(xNode.Attributes["created"].Value.ToString()),
        DateTime.Today, DateTime.Today, DateTime.Today, 
        DateTime.MinValue);
      }
    }
  }
catch
{
  // Take appropriate action.
}

  // Return the results.
  return ret;
}

public override MembershipUser GetUser(string username, 
bool userIsOnline)
{
  MembershipUser ret = null;
  XmlNode xNode = null;

  try
  {
    // Get the XML document.
    xDoc = Helper.GetXmlFile();

    // Proceed if it contains something.
    if (xDoc != null)
    {

    // Look for a user with a name that matches.
    xNode = xDoc.SelectSingleNode("/Users/User[@name='" + 
      username + "']");

    // See if there are any matches.
    if (xNode != null)
    {
      // Create a new membershipusercollection.
      ret = new MembershipUser(Membership.Provider.Name,
      xNode.Attributes["name"].Value.ToString(),
      xNode.Attributes["name"].Value.ToString(),
      xNode.Attributes["email"].Value.ToString(),
      string.Empty, string.Empty, true, false,
      DateTime.Parse(xNode.Attributes["created"].Value.ToString()),
      DateTime.Today, DateTime.Today, DateTime.Today,
      DateTime.MinValue);
    }
  }
}
catch
{
  // Take appropriate action.
}

  // Return the results.
  return ret;
}
Public Overloads Overrides Function GetUser(_ 
ByVal providerUserKey As Object, ByVal userIsOnline As Boolean) _
As System.Web.Security.MembershipUser

Dim ret As MembershipUser = Nothing
Dim xNode As XmlNode = Nothing

Try
  ' Get the XML document.
  xDoc = Helper.GetXmlFile()

  ' Proceed if it contains something.
  If xDoc IsNot Nothing Then

    ' Look for a user with a name that matches.
    xNode = xDoc.SelectSingleNode("/Users/User[@name='" & _
      providerUserKey.ToString() & "']")

    ' See if there are any matches.
    If xNode IsNot Nothing Then
      'Create a new membershipusercollection.
        ret = New MembershipUser(Membership.Provider.Name, _
        xNode.Attributes("name").Value.ToString(), _
        xNode.Attributes("name").Value.ToString(), _
        xNode.Attributes("email").Value.ToString(), _
        String.Empty, String.Empty, True, False, _
        Date.Parse(xNode.Attributes("created").Value.ToString()), _
        Date.Today, Date.Today, Date.Today, Date.MinValue)
    End If
  End If

Catch ex As Exception
  'Take appropriate action.
End Try

' Return the results.
Return ret
End Function

Public Overloads Overrides Function GetUser( _
ByVal username As String,ByVal userIsOnline As Boolean) _
As System.Web.Security.MembershipUser

Dim ret As MembershipUser = Nothing
Dim xNode As XmlNode = Nothing

Try
  ' Get the XML document.
  xDoc = Helper.GetXmlFile()

  ' Proceed if it contains something.
  If xDoc IsNot Nothing Then

  ' Look for a user with a name that matches.
  xNode = xDoc.SelectSingleNode("/Users/User[@name='" & _
    username & "']")

  ' See if there are any matches.
  If xNode IsNot Nothing Then
    ' Create a new membershipusercollection.
    ret = New MembershipUser(Membership.Provider.Name, _
    xNode.Attributes("name").Value.ToString(), _
    xNode.Attributes("name").Value.ToString(), _
    xNode.Attributes("email").Value.ToString(), _
    String.Empty, String.Empty, True, False, _
                
    Date.Parse(xNode.Attributes("created").Value.ToString()), _
    Date.Today, Date.Today, Date.Today, Date.MinValue)
    End If
  End If

Catch ex As Exception
  'Take appropriate action.
End Try

' Return the results.
Return ret
End Function 

GetUserNameByEmail Method

The GetUserNameByEmail function takes an e-mail address as a parameter and looks for a user with that e-mail address. If it finds such a user, it returns the user name.

public override string GetUserNameByEmail(string email)
{
  string ret = string.Empty;
  XmlNode xNode = null;

  try
  {
    // Get the XML document.
    xDoc = Helper.GetXmlFile();

    // Proceed if it contains something.
    if (xDoc != null)
    {
      // Look for the user.
      xNode = xDoc.SelectSingleNode("/Users/User[@email='" + 
        email + "']");

      // See if it found a match.
      if (xNode != null) 
        ret = xNode.Attributes["name"].Value.ToString();
    }
  }
  catch 
  {
    // Take appropriate action.
  }

  // Return the results.
  return ret;
}
Public Overrides Function GetUserNameByEmail(ByVal email As String) _
As String

Dim ret As String = String.Empty
Dim xNode As XmlNode = Nothing

Try
  ' Get the XML document.
  xDoc = Helper.GetXmlFile()

  ' Proceed if it contains something.
  If xDoc IsNot Nothing Then

    ' Look for the user.
    xNode = xDoc.SelectSingleNode("/Users/User[@email='" & _
      email & "']")

    ' See if it found a match.
    If xNode IsNot Nothing Then _ 
      ret = xNode.Attributes("name").Value.ToString
  End If

Catch ex As Exception
  ' Take appropriate action.
End Try

' Return the results.
Return ret
End Function

ValidateUser Method

The ValidateUser function takes a user name and password and verifies whether it is correct.

public override bool ValidateUser(string username, string password)
{

  bool ret = false;
  XmlNode xNode = null;

  try
  {
    // Get the XML document.
    xDoc = Helper.GetXmlFile();

    // Proceed if it contains something.
    if (xDoc != null)
    {
      // Look for the user.
      xNode = xDoc.SelectSingleNode("/Users/User[@name='" + 
        username + "']");

      // See if it found a match.
      if (xNode != null)
      {
        // Look for the password attribute to see if it matches.
        if (xNode.Attributes["password"].Value.ToString() == password) 
          ret = true;
      }
    }
  }
  catch
  {
    // Take appropriate action.
  }

  // Return the results.
  return ret;
}
Public Overrides Function ValidateUser(ByVal username As String, _
ByVal password As String) As Boolean

Dim ret As Boolean = False
Dim xNode As XmlNode = Nothing

Try
  ' Get the XML document.
  xDoc = Helper.GetXmlFile()

  ' Proceed if it contains something.
  If xDoc IsNot Nothing Then

    ' Look for the user.
    xNode = xDoc.SelectSingleNode("/Users/User[@name='" & _
      username & "']")

    ' See if it found a match.
    If xNode IsNot Nothing Then
      ' Look for the password attribute to see if it matches.
      If xNode.Attributes("password").Value.ToString = password Then _
        ret = True
    End If
  End If

Catch ex As Exception
  ' Take appropriate action.
End Try

' Return the results.
Return ret
End Function 

FindUsersByEmail Method

The FindUsersByEmail function takes an e-mail address as a parameter and finds all users whose e-mail addresses start with that value. It returns a System.Web.Security.MembershipUserCollection object.

public override MembershipUserCollection FindUsersByEmail(string 
emailToMatch, int pageIndex, int pageSize, out int totalRecords)
{
  MembershipUserCollection ret = null;
  XmlNodeList xList = null;

  // Initialize the number of records found.
  totalRecords = 0;

  try
  {

    // Get the XML document.
    xDoc = Helper.GetXmlFile();

    // Proceed if it contains something.
    if (xDoc != null)
    {
      // Look for users with matching e-mail addresses.
      xList = xDoc.SelectNodes("/Users/User[starts-with(@email, '" + 
        emailToMatch + "')]");

      // See if there are any matches.
      if ((xList != null) && (xList.Count > 0))
      {
        // Set the number of records found.
        totalRecords = xList.Count;

        // Create a new membershipusercollection.
        ret = new MembershipUserCollection();

        // Enumerate each match and add it to the collection.
        foreach (XmlNode xNode in xList)
        {
          ret.Add(new MembershipUser(Membership.Provider.Name,
          xNode.Attributes["name"].Value.ToString(), 
          xNode.Attributes["name"].Value.ToString(), 
          xNode.Attributes["email"].Value.ToString(), 
          string.Empty, string.Empty, true, false, 
          DateTime.Parse(xNode.Attributes["created"].Value.ToString()),
          DateTime.Today, DateTime.Today, DateTime.Today,
          DateTime.MinValue));
        }
      }
    }
  }
  catch
  {
    // Take appropriate action.
  }

  // Return the results.
  return ret;
}
Public Overrides Function FindUsersByEmail(ByVal emailToMatch As _
String, ByVal pageIndex As Integer, ByVal pageSize As Integer, _
ByRef totalRecords As Integer) As _
System.Web.Security.MembershipUserCollection

Dim ret As MembershipUserCollection = Nothing
Dim xList As XmlNodeList = Nothing

' Initialize the number of records found.
totalRecords = 0

Try
  ' Get the XML document.
  xDoc = Helper.GetXmlFile()

  ' Proceed if it contains something.
  If xDoc IsNot Nothing Then

    ' Look for users with matching e-mail addresses.
    xList = xDoc.SelectNodes("/Users/User[starts-with(@email, '" & _
      emailToMatch & "')]")

    ' See if there are any matches.
    If xList IsNot Nothing AndAlso xList.Count > 0 Then
      ' Set the number of records found.
      totalRecords = xList.Count

      ' Create a new membershipusercollection.
      ret = New MembershipUserCollection()

      ' Enumerate each match and add it to the collection.
      For Each xNode As XmlNode In xList
        ret.Add(New MembershipUser(Membership.Provider.Name, _
        xNode.Attributes("name").Value.ToString(), _
        xNode.Attributes("name").Value.ToString(), _
        xNode.Attributes("email").Value.ToString(), _
        String.Empty, String.Empty, True, False, _
        Date.Parse(xNode.Attributes("created").Value.ToString()), _
        Date.Today, Date.Today, Date.Today, Date.MinValue))
      Next
    End If
  End If

Catch ex As Exception
  ' Take appropriate action.
End Try

' Return the results.
Return ret
End Function

FindUsersByName Method

The FindUserByName function works the same way that the FindUserByEmail function does; however, it takes a user name (full or partial) as a parameter.

public override MembershipUserCollection FindUsersByName(string 
usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
{

  MembershipUserCollection ret = null;
  XmlNodeList xList = null;

  // Initialize the number of records found.
  totalRecords = 0;

  try
  {

    // Get the XML document.
    xDoc = Helper.GetXmlFile();

    // Proceed if it contains something.
    if (xDoc != null)
    {
      // Look for users with matching e-mail addresses.
      xList = xDoc.SelectNodes("/Users/User[starts-with(@name, '" + 
        usernameToMatch + "')]");

      // See if there are any matches.
      if ((xList != null) && (xList.Count > 0))
      {
        // Create a new membershipusercollection.
        ret = new MembershipUserCollection();

        // Enumerate each match and add it to the collection.
        foreach (XmlNode xNode in xList)
        {
          ret.Add(new MembershipUser(Membership.Provider.Name,
          xNode.Attributes["name"].Value.ToString(),
          xNode.Attributes["name"].Value.ToString(),
          xNode.Attributes["email"].Value.ToString(),
          string.Empty, string.Empty, true, false,
          DateTime.Parse(xNode.Attributes["created"].Value.ToString()),
          DateTime.Today, DateTime.Today, DateTime.Today, 
          DateTime.MinValue));
        }
      }
    }
  }
  catch
  {
    // Take appropriate action.
  }

  // Return the results.
  return ret;
}
Public Overrides Function FindUsersByName( _
ByVal usernameToMatch As String, ByVal pageIndex As Integer, _
ByVal pageSize As Integer, ByRef totalRecords As Integer) _
As System.Web.Security.MembershipUserCollection

Dim ret As MembershipUserCollection = Nothing
Dim xList As XmlNodeList = Nothing

' Initialize the number of records found.
totalRecords = 0

Try
  ' Get the XML document.
  xDoc = Helper.GetXmlFile()

  ' Proceed if it contains something.
  If xDoc IsNot Nothing Then

    ' Look for users with matching e-mail addresses.
    xList = xDoc.SelectNodes("/Users/User[starts-with(@name, '" & _
      usernameToMatch & "')]")

    ' See if there are any matches.
    If xList IsNot Nothing AndAlso xList.Count > 0 Then
      'create a new membershipusercollection
      ret = New MembershipUserCollection()

      ' Enumerate each match and add it to the collection.
      For Each xNode As XmlNode In xList
        ret.Add(New MembershipUser(Membership.Provider.Name, _
          xNode.Attributes("name").Value.ToString(), _
          xNode.Attributes("name").Value.ToString(), _ 
          xNode.Attributes("email").Value.ToString(), _
          String.Empty, String.Empty, True, False, _
          Date.Parse(xNode.Attributes("created").Value.ToString()), _
          Date.Today, Date.Today, Date.Today, Date.MinValue))
      Next
    End If
  End If

Catch ex As Exception
  ' Take appropriate action.
End Try

' Return the results.
Return ret
End Function

Functions Required in a Custom Role Provider

The following sections describe functions (methods) that are required in a custom role provider.

GetRolesForUser Method

The GetRolesForUser function takes a user name as a parameter and returns a string array that contains the names of all groups to which the user belongs.

Important

You must understand one limitation when you are writing code for the GetRolesForUser method. The role provider has a property named System.Web.Security.Roles.CacheRolesInCookie. Unfortunately, because of a bug that exists in ASP.NET at the time this article was written, that attribute is not honored, and the role provider's GetRolesForUser method is called every time. In practice, the GetRolesForUser method is called at least once for every user, for every page that he or she visits in the SharePoint site. Because of this, you should implement your own caching mechanism in the GetRolesForUser method in any custom role provider.

public override string[] GetRolesForUser(string username)
{

  string[] ret = null;
  XmlNode xNode = null;
  XmlNodeList xList = null;

  try
  {
    // Get the XML document.
    xDoc = Helper.GetXmlFile();

    // Proceed if it contains something.
    if (xDoc != null)
    {
      // Look for the user.
      xNode = xDoc.SelectSingleNode("/Users/User[@name='" + 
        username + "']");

      // See if it found a match.
      if (xNode != null)
      {
        // Get the collection of group nodes.
        xList = xNode.ChildNodes[0].ChildNodes;

        // Resize the array based on number of child nodes.
        ret = new string[xList.Count];

        // Enumerate all the groups in the Groups/Group subnodes 
        // and add to return value.
        for (int cnt = 0;cnt < xList.Count;cnt++)
        {
          ret[cnt] = xList.Item(cnt).InnerText;
        }
      }
    }
  }
  catch
  {
    // Take appropriate action.
  }
  
  //Return the results.
  return ret;
}
Public Overrides Function GetRolesForUser(ByVal username As String) _
As String()

Dim ret As String() = Nothing
Dim xNode As XmlNode = Nothing
Dim xList As XmlNodeList = Nothing

Try
  ' Get the XML document.
  xDoc = Helper.GetXmlFile()

  ' Proceed if it contains something.
  If xDoc IsNot Nothing Then

    ' Look for the user.
    xNode = xDoc.SelectSingleNode("/Users/User[@name='" & _
      username & "']")

    ' See if it found a match.
    If xNode IsNot Nothing Then
      ' Get the collection of group nodes.
      xList = xNode.ChildNodes(0).ChildNodes

      ' Resize the array based on number of child nodes.
      ReDim ret(xList.Count - 1)

      ' Enumerate all the groups in the Groups/Group subnodes and 
      ' add to return value.
      For cnt As Integer = 0 To xList.Count - 1
        ret(cnt) = xList.Item(cnt).InnerText
      Next
    End If
  End If

Catch ex As Exception
  ' Take appropriate action.
End Try

' Return the results.
Return ret
End Function

RoleExists Method

The RoleExists function takes a role name as a parameter. If the role exists, the function returns true; otherwise, it returns false.

public override bool RoleExists(string roleName)
{
  bool ret = false;
  XmlNode xNode = null;

  try
  {
    // Get the XML document.
    xDoc = Helper.GetXmlFile();

    // Proceed if it contains something.
    if (xDoc != null)
    {
      // Look for the role.
      xNode = xDoc.SelectSingleNode("/Users/User[Groups/Group='" + 
        roleName + "']");

      // Return true if it found a match.
      if (xNode != null) ret = true;
    }
  }
  catch
  {
    // Take appropriate action.
  }

  // Return the results.
  return ret;
}
Public Overrides Function RoleExists(ByVal roleName As String) _
As Boolean

Dim ret As Boolean = False
Dim xNode As XmlNode = Nothing

Try
  ' Get the XML document.
  xDoc = Helper.GetXmlFile()

  ' Proceed if it contains something.
  If xDoc IsNot Nothing Then

    ' Look for the role.
    xNode = xDoc.SelectSingleNode("/Users/User[Groups/Group='" & _
      roleName & "']")

    ' Return true if it found a match.
    If xNode IsNot Nothing Then ret = True
  End If

Catch ex As Exception
  ' Take appropriate action.
End Try

' Return the results.
Return ret
End Function

Registering the Custom Provider

Now that you understand the minimum required interfaces for a custom Membership and Role provider to work with Office SharePoint Server 2007 and Windows SharePoint Services 3.0. your next step is to register the custom provider.

To be able to use a custom provider, you must provide a strong name for the assembly it uses, and then register it in the global assembly cache.

To strong name the assembly

  1. In Microsoft Visual Studio, right-click the project name, and then click Properties.

  2. Click the Signing tab, and then select Sign the assembly.

  3. In the Choose a strong name key file list, click <New…>.

  4. In the Create Strong Name Key dialog box, type a name for the key file, optionally type a password for the key file, and then click OK.

When you compile the assembly, it builds with the strong name you provided.

You can now add your strong-named assembly to the global assembly cache in several ways, but the most common way is to use the gacutil.exe utility with the /i option. For more information about installing an assembly in the global assembly cache, see How to: Install an Assembly into the Global Assembly Cache.

After you register the assembly, you can extend a Web application into a zone and configure it to use your custom provider (described in the "Setting Up Forms Authentication" section of Forms Authentication in SharePoint Products and Technologies (Part 1): Introduction).

Following are two different example entries for the custom provider, reflecting that the Microsoft Visual C# and Microsoft Visual Basic assemblies were created with different names. In the examples, when you configure the authentication provider for the zone, the authentication method should be Forms, the membership provider name should be fbaUser, and the role manager name should be fbaRole.

Note

Because the strong name for an assembly that you compile on your computer will differ from the following example, the entries for your custom provider will also differ slightly; specifically the PublicKeyToken value will be different.

For the Visual C# assembly, use the following code.

    <membership defaultProvider="fbaUser">
      <providers>
        <add name="fbaUser" type="Microsoft.IW.customUser, fbaSharp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b74d22b2d68547b5" />
      </providers>
    </membership>
    <roleManager enabled="true" defaultProvider="fbaRole">
      <providers>
        <add name="fbaRole" type="Microsoft.IW.customRole, fbaSharp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b74d22b2d68547b5" />
      </providers>
    </roleManager>

For the Visual Basic assembly, use the following code.

    <membership defaultProvider="fbaUser">
      <providers>
        <add name="fbaUser" type="Microsoft.IW.customUser, fbaVB, Version=1.0.0.0, Culture=neutral, PublicKeyToken=ddd85720e6ace42b" />
      </providers>
    </membership>
    <roleManager enabled="true" defaultProvider="fbaRole">
      <providers>
        <add name="fbaRole" type="Microsoft.IW.customRole, fbaVB, Version=1.0.0.0, Culture=neutral, PublicKeyToken=ddd85720e6ace42b" />
      </providers>
    </roleManager>

Debugging the Custom Provider

Now that you have compiled the assembly, registered it, and configured a new zone to use the custom provider, you can use and (optionally) debug the provider. You would debug the custom provider in the same way that you would debug any other assembly that is used by another process. In this case, the process that is consuming the assembly is the w3wp.exe process.

To debug the provider, you set the breakpoints you want in the code, and then attach to the w3wp.exe process.

To attach to the w3wp.exe

  1. In Visual Studio, on the Tools menu, click Attach to Process to open the Attach to Process dialog box.

  2. In Available Processes, select the w3wp.exe process, and then click Select.

    • If you do not see any w3wp processes, select Show processes from all users.

    • Also, if you have recently performed an iisreset, you must navigate to a page in the SharePoint site to create a new w3wp.exe process. Conversely, you might also find multiple w3wp.exe processes running. In such a case, select all w3wp.exe processes. This ensures that you attach to the process that is running your provider assembly.

After you attach the w3wp.exe process, try logging on to the site. If you attached a breakpoint on the ValidateUser method of your membership provider, the breakpoint should be hit at the time you log on. If the breakpoint is not hit, you should try reattaching to the w3wp.exe process; another process might have started in connection with logging on to the site. The processes that are already attached appear dimmed (they are disabled); any new w3wp.exe processes appear black and are enabled to allow attachments.

As you log on and navigate to pages, you should see breakpoints being hit in both the membership and role providers. To test your providers further, try using the People Picker to search for users and roles to add to SharePoint groups. This triggers breakpoints set in any of the membership and role provider methods. The number of times the breakpoints are hit varies on what is being done. When you use the People Picker, some methods such as FindUserByEmail can be called more than once. The number of times the role provider is called for any given page can vary also. The role provider is called for each item on the page that requires authentication. Some examples of items are navigation, list view Web Parts, or parts that are targeted at audiences. As a result, the GetRolesForUser method in the role provider is called one or more times for every user on each page view.

Figure 1 and Figure 2 show the People Picker, configured to use the custom membership and role providers. Notice the account name for the entities matches the name attribute of the membership and role provider, as described earlier.

Figure 1. Select People and Groups - membership provider name

Select People and Groups- membership provider name

Figure 2. Select People and Groups - role provider name

Select People and Groups - role provider name

Writing a Custom Forms Logon Page

You may have scenarios that have special logon requirements that cannot be addressed by the default SharePoint forms logon page. For example, you may need to implement a second authentication factor such as Secure ID. Fortunately, because SharePoint Products and Technologies are built on top of ASP.NET 2.0, you can create a custom logon page with your own logon logic, and integrate it directly into Office SharePoint Server or Windows SharePoint Services.

Creating a Standard ASP.NET Web Site

The easiest way to create a custom logon page is to build a standard ASP.NET Web application. This enables you to create a site and easily debug the code behind for your forms logon page to ensure that it is working correctly. When you do this, you should configure your site to use the same membership and role provider that you intend to use with Office SharePoint Server or Windows SharePoint Services.

In the scenario for this article, we built a custom forms logon page for two reasons:

  • To show a custom policy condition that all site users must agree to before using the site.

  • To force certain users to use two-factor authentication, where the second factor is Secure ID.

We have created an ASPX page named customLogin.aspx and added it to the site. It contains the following:

  • Edit boxes for user name, password, and Secure ID number

  • A check box for a persistent authorization cookie

  • A button to perform the logon process

Figure 3 shows how the page should look.

Figure 3. customLogin.aspx page

customLogin.aspx page

The web.config file was modified to do the following:

  • Use forms authentication and the fbaVB membership and role provider described earlier in this article, in the section Registering the Custom Provider.

  • Deny access to anonymous users.

Default.aspx was also added to the site when the project was created, and it is used in the site for testing.

Adding Code Behind the Logon Page

In the code behind the logon page, you should first validate the credentials. You can use the ValidateUser method of the Membership class to do this. If the credentials are valid, you can redirect the user to the site; otherwise, prompt them to reenter their credentials.

if (Membership.ValidateUser(UserTxt.Text, PwdTxt.Text))
{
  // Do Secure ID thing here. 
  // Redirect the user to the requested page.
  FormsAuthentication.RedirectFromLoginPage(UserTxt.Text,
    SaveChk.Checked);
}
else 
  StatusLbl.Text = "The credentials you entered are not valid. " + 
  "Please try again.";
If Membership.ValidateUser(UserTxt.Text, PwdTxt.Text) Then
  ' Do Secure ID thing here. 
  ' Redirect the user to the requested page.
  FormsAuthentication.RedirectFromLoginPage(UserTxt.Text, _
    SaveChk.Checked)
Else
  StatusLbl.Text = "The credentials you entered are not valid. " & _
    Please try again."
End If

For the Secure ID component, this example simulates only the process that a user or another system that requires additional authentication processing would go through. In this example, the page is hard-coded to prompt a specific user for a Secure ID number; in practice you could query a database or Web service (or whatever is appropriate) to determine how to process users. After prompting the user for the Secure ID credentials, it allows them to pass.

// Instead of redirecting user3, ask for a Secure ID number. 
if (UserTxt.Text.ToLower() == "user3")
{
  // Update UI.
  StatusLbl.Text = "Please enter your Secure ID number";
  LogPnl.Visible = false;
  SecurePnl.Visible = true;
}
' Instead of redirecting user3, ask for a Secure ID number. 
If UserTxt.Text.ToLower() = "user3" Then
  ' Update UI.
  StatusLbl.Text = "Please enter your Secure ID number"
  LogPnl.Visible = False
  SecurePnl.Visible = True
End If

Compiling the Application into an Assembly

After you complete the simple ASP.NET application and have the forms logon page working correctly, your next step is to compile it and use it in Office SharePoint Server or Windows SharePoint Services. You can use the Web site project that is new with Visual Studio 2005 to create the Web application. Alternatively, with Service Pack 1 of Visual Studio you can also create the Web project model that is similar to Visual Studio 2003, in that you can compile the project into a single assembly (.dll file).

Note

It is often easier to debug integrated solutions such as this if you use the latter approach and compile the Web application into a single DLL.

For the remainder of this section, we assume that you have compiled the project into a single assembly. If you use the Visual Studio 2005 style of Web site project, you can still make the application work, but you must use the ASP.NET Compilation Tool (Aspnet_compiler.exe) to precompile it. If you use that approach, ensure that you include the -u parameter when you compile so that you can update the assembly later.

Copying Files and Registering the Assembly

Following are the steps to make the custom logon page available to Office SharePoint Server or Windows SharePoint Services.

To enable the use of the custom logon page by SharePoint Products and Technologies

  1. Copy the logon page to the _layouts directory. The default path is C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\LAYOUTS.

  2. Create a bin directory on the file system at the top-level (root) of your SharePoint Web application that will use your custom logon page. For the examples used in this article, the default directory would be in the path C:\Inetpub\wwwroot\wss\VirtualDirectories\ www.contoso.com80. Then, copy the compiled assembly for the custom forms application to the bin directory that you created.

  3. Register the compiled assembly for the custom forms application in the global assembly cache. You can do this by using the Global Assembly Cache Tool (Gacutil.exe) with the /i option. For more details about installing an assembly in the global assembly cache, see How to: Install an Assembly into the Global Assembly Cache.

You must also update the web.config file for the Web application that is going to use the custom logon page.

To update the web.config file for the Web application

  1. Open the web.config file.

  2. In the authentication section, locate the forms element.

  3. In the forms element, change the loginUrl attribute to point to your new custom forms logon page; for example, _layouts/customLogin.aspx.

  4. Perform an iisreset.

    Note

    This step is important; things might not work correctly if you do not perform an iisreset.

You should now be able to use the custom logon page.

Following is an example of what the page looks like when used with the Contoso site described earlier in this article.

Figure 4. customLogin.aspx page on Contoso site

customLogin.aspx page on Contoso site

The interface is obviously not quite what one would expect from a SharePoint site. Fortunately, you can copy elements from the login.aspx page that is included with SharePoint Products and Technologies and paste them into the custom logon page to give it the SharePoint appearance.

To modify the custom logon page to give it the SharePoint appearance

  1. In the @ Page directive at the top of the page, add the following master page attribute.

    MasterPageFile="~/_layouts/simple.master"
    
  2. Remove the following lines from near the top of the page.

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head id="Head1" >
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" >
    
  3. Add the following lines directly below the @ Page directive.

    <%@ Import Namespace="Microsoft.SharePoint.ApplicationPages" %> 
    <%@ Register Tagprefix="SharePoint" 
    Namespace="Microsoft.SharePoint.WebControls" 
    Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, 
    PublicKeyToken=71e9bce111e9429c" %> 
    <%@ Register Tagprefix="Utilities" 
    Namespace="Microsoft.SharePoint.Utilities" 
    Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> 
    <%@ Import Namespace="Microsoft.SharePoint" %>
    <asp:Content ContentPlaceHolderId="PlaceHolderPageTitle" >
        <SharePoint:EncodedLiteral  text="<%$Resources:wss,login_pagetitle%>" EncodeMethod='HtmlEncode'/>
    </asp:Content>
    <asp:Content ContentPlaceHolderId="PlaceHolderTitleBreadcrumb" >
    &nbsp;
    </asp:Content>
    <asp:Content ContentPlaceHolderId="PlaceHolderPageTitleInTitleArea" >
        <SharePoint:EncodedLiteral  text="<%$Resources:wss,login_pagetitle%>" EncodeMethod='HtmlEncode'/>
    </asp:Content>
    <asp:Content ContentPlaceHolderId="PlaceHolderSiteName" />
    <asp:Content ContentPlaceHolderId="PlaceHolderMain" >
    
  4. Remove the following lines from the bottom of the page.

    </form>
    </body>
    </html>
    
  5. Add the following line to the bottom of the page.

    </asp:Content>
    

Try logging on to the site again; you may need to refresh the page. After you refresh, the page should look like Figure 5 and Figure 6.

Figure 5. Updated customLogin.aspx on Contoso site - enter user name and password

Updated customLogin.aspx on Contoso site

Figure 6. Updated customLogin.aspx on Contoso site - enter Secure ID

Updated customLogin.aspx on Contoso site

Using Web Services with a Site Protected by Forms Authentication

Using the SharePoint Web services with a site secured with forms authentication works; however, the process differs from what you would do to use Web services for a site that is secured with Windows authentication. The primary difference is that you must obtain an authentication cookie and then use that cookie when accessing the Web services in the site protected by forms authentication.

Fortunately, SharePoint Products and Technologies provide a new Web service that makes it easier to work in this scenario: the Authentication Web service. It has a method named Login that, when called, places an authentication cookie in the proxy's System.Web.Services.Protocols.HttpWebClientProtocol.CookieContainer collection. That cookie can then be used in subsequent requests to other Web services in the site that is protected by forms authentication to authenticate the request.

Note

By default, a zone that is configured to use forms authentication does not enable Client Integration features. This option, which is found on the Authentication Provider page in SharePoint Central Administration, must be turned on if you want to use the SharePoint Web services. When this option is turned off, SharePoint Products and Technologies also turn off support for remote interfaces, such as Web services.

For purposes of creating your Web service proxies (such as adding Web references in Visual Studio), use a Windows authentication–protected site. In most cases, the Visual Studio Add Web Reference wizard does not work with a SharePoint site that is protected by forms authentication. For this example, we retrieve all of the lists in the site by using the Lists Web service.

To retrieve all lists in the site by using the Lists Web services

  1. Start Visual Studio, and create a Windows Application project.

  2. Add a button and text box to Form1.

  3. Change the text box properties so that Multiline is True and Scrollbars is Vertical.

  4. Resize the text box to fill the form under the button.

  5. Add a Web reference to the Authentication Web service in the site that is protected by forms authentication; the Authentication Web service can be found in the path http://siteCollectionName/_vti_bin/authentication.asmx. Name this Web reference fbaAuth.

  6. Add a second Web reference to the Lists Web service; it can be found in the path http://siteCollectionName/_vti_bin/lists.asmx. Name this Web reference fbaLists.

  7. On the form, double-click Button1 to switch to code view and create an event handler for the click event. Create two variables for the Web service proxies, as shown in the following code.

    fbaAuth.Authentication auth = new fbaAuth.Authentication();
    fbaLists.Lists lists = new fbaLists.Lists();
    
    Dim auth As New fbaAuth.Authentication()
    Dim lists As New fbaLists.Lists()
    
  8. Create a CookieContainer collection object for the Authentication class proxy; the authentication cookie will be stored in this container after calling the Login method.

  9. Call the Login method, and check the result from that call, as shown in the following code.

    auth.CookieContainer = new System.Net.CookieContainer();
    auth.AllowAutoRedirect = true;
    fbaAuth.LoginResult lr = auth.Login("myUserName", "myUserPassword");
    
    if (lr.ErrorCode == fbaAuth.LoginErrorCode.NoError)
    {
      //Now we can talk to the Lists Web service.
    }
    
    auth.CookieContainer = New System.Net.CookieContainer()
    auth.AllowAutoRedirect = True
    Dim lr As fbaAuth.LoginResult = auth.Login("myUserName", _
      "myUserPassword")
    
    If lr.ErrorCode = fbaAuth.LoginErrorCode.NoError Then
      'Now we can talk to the Lists Web service.
    End If
    

If the Login method succeeds, the proxy for the Authentication Web service has a valid authentication cookie in its CookieContainer collection. To reuse this cookie, just set the CookieContainer property for the Lists Web service proxy equal to the CookieContainer property of the Authentication Web service proxy. You can then make calls to the Lists Web service, and the authentication cookie will be used to authenticate and authorize the request. That means that when you make a Web service request, the cookie with authenticate and authorize the request based on the permissions of whatever account was used to create the authentication cookie.

Following is the remainder of the code sample.

if (lr.ErrorCode == fbaAuth.LoginErrorCode.NoError)
{
   // Now we can talk to the Lists Web service.
   lists.CookieContainer = auth.CookieContainer;
   XmlNode xData = lists.GetListCollection();
}
If lr.ErrorCode = fbaAuth.LoginErrorCode.NoError Then
   ' Now we can talk to the Lists Web service.
   lists.CookieContainer = auth.CookieContainer
   Dim xData As XmlNode = lists.GetListCollection()
End If

This concludes part 2 of this article series.

Next step:Forms Authentication in SharePoint Products and Technologies (Part 3): Forms Authentication vs. Windows Authentication.

Additional Resources

For more information, see the following resources: