Pages

Sunday, November 2, 2008

How To Create and Add Users to Groups using an HttpModule for a Forms Authentication (FBA) SharePoint Site


The aim of this project was to create a HttpModule for a Forms Authentication (FBA) SharePoint site that would authenticate the user through Windows Live and then add the user to a group in SharePoint. It would also save built-in WSS and MOSS custom user profile information and update the profile information on every login. Potentially the site would have to handle about 100,000 users.

Requirements:

- Designed for cross domain servers
- Authenticate the user through Windows Live
- Use FormsAuthentication cookie to pass back user profile information
- Add the user to a group in SharePoint
- Save built-in WSS and MOSS custom user profile information
- Update built-in WSS and MOSS custom user profile information after every login

After several weeks of research I was finally able to create a FBA SharePoint site that add users to a group in SharePoint with all the other requirements. The trick was creating a dummy Membership Provider which I didn't need since I was adding the users to groups with code rather then through the People Picker. The dummy Membership Provider is only needed to satisfy the required Membership Provider Name used in the Sharepoint "Authentication Providers > Edit Authentication" setup when you change the "Authentication Type" to "Forms Authentication". I found an article Guest Account Enabler by Reza Alirezaei of significant help. He created an HttpModule using a Minimal Membership Provider that provides a "Guest" user account that can be manually added through SharePoint's "People and Groups / Add Users". I took his work and stripped down the Minimal Membership Provider to a bare shell because there wasn't a need for it in my requirements.


Implementation

My SharePoint portal has 2 host mappings for the Default Zone and the Internet Zone:

LiveIDsp SharePoint Windows Authenticated page [Default Zone]:

- http://LiveIDsp.moss.local:8080/ for Windows authenticated users.

LiveIDsp SharePoint Forms Authenticated Internet page [Internet Zone]:

- http://LiveIDsp.moss.local:80/ for Live ID authenticated users.


I also include a login page to simulate Live ID logins. It uses a FormsAuthentication cookie and is designed to cross domain servers using a second level domain name of moss.local.

LiveID Forms Authenticated Internet page:

- http://LiveID.moss.local:80


Project Setup using SharePoint Web Services

Add the following 2 web references to your project using the Default Zone URL:

- [ws_UserGroup] = http://[defaultsitename]:[port]/_vti_bin/usergroup.asmx
- [ws_UserProfileService] = http://[defaultsitename]:[port]/_vti_bin/userprofileservice.asmx

Custom User Profile Properties

I have included code to update custom profile properties to both the WSS site profiles and MOSS global profiles. Both are commented out. If you only want to use the WSS site profiles, then you can use the AddWSSCustomProfileProperties code to create the WSS custom profile properties and install it using Features. However, the easiest way to create custom profile properties in both profiles is to use SSPAdmin (Shared Services Provider Administrator) to create the custom profile properties in the MOSS Profile Database. You will find this in SSPAdmin under "User Profiles and Properties/Add Profile Properties". Once the custom profile properties are created, my code will add the data and keep them synchronized when the user logs in, which you may know, can be problematic if left up to SharePoint to synchronize. The custom properties I created are:

- SiteID [Integer]
- SiteName [String(80)]
- Region [String(80)]
- Created [Date Time] (For MOSS profile only since this already exists as a built-in field in WSS)
- Modified [Date Time] (For MOSS profile only since this already exists as a built-in field in WSS)


Please read the Reademe.txt for additional setup information.

//---------------------------------------------------------------------
//Readme.txt:
//---------------------------------------------------------------------

Readme:
1) Add the following Web.config modifications to the default Windows Authentication (NTLM) zone.

    <membership defaultProvider="LiveID" userIsOnlineTimeWindow="15">
      <providers>
        <add name="LiveID" type="LiveIDHttpModule."LiveIDMembershipProvider, LiveIDHttpModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=7bdf16a60eb5397d" applicationName="/" passwordFormat="Clear" description="Authenticates users against "LiveIDMembershipProvider" connectionstring="MyConnectionString" />
      </providers>
    </membership>

    <connectionStrings>
      <add name="MyConnectionString" connectionString="Data Source=myserver;Initial Catalog=mydb;User Id=USER1;Password= pass@word!;" providerName="System.Data.SqlClient" />
    </connectionStrings>

2) Add the following Web.config modifications to the extended Forms Authentication (FBA) internet zone.

    <authentication mode="Forms">
      <forms name=".LiveIDAUTH" protection="All" timeout="60" loginUrl="http://LiveID.moss.local/LiveIDlogin.aspx" />
    </authentication>
    <membership defaultProvider=""LiveID" userIsOnlineTimeWindow="15">
      <providers>
        <add name=""LiveID" type="LiveIDHttpModule."LiveIDMembershipProvider, LiveIDHttpModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=7bdf16a60eb5397d" applicationName="/" passwordFormat="Clear" description="Authenticates users against "LiveIDMembershipProvider" connectionstring="MyConnectionString" />
      </providers>
    </membership>

    <connectionStrings>
      <add name="MyConnectionString" connectionString="Data Source=myserver;Initial Catalog=mydb;User Id=USER1;Password= pass@word!;" providerName="System.Data.SqlClient" />
    </connectionStrings>

3) Add the following Web.config modifications to the extended (FBA) zone in the Http Module <httpModules> section.

    <add name="LiveIDModule" type="LiveIDHttpModule.LiveIDModule,LiveIDHttpModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=7bdf16a60eb5397d" />

4) Add to following Web.config appSettings to the extended (FBA) zone.

    <appSettings>
      <add key="Level2DomainName" value="moss.local"/>
      <add key="LiveIDLoginSite" value="http://LiveID.moss.local"/>
      <add key="LiveIDSPSite" value="http://LiveIDsp.moss.local:8080"/>
      <add key="LiveIDSPSiteInternet" value="http://LiveIDsp.moss.local"/>
      <add key="LiveIDUserGroup" value="Viewers"/>
    </appSettings>

5) Add a Web Resource called ws_UserGroup at the following web site.

    http://LiveIDsp.moss.local:8080/_vti_bin/usergroup.asmx

6) Add a Web Resource called ws_UserProfileService at the following web site.

    http://liveidsp.moss.local:8080/_vti_bin/userprofileservice.asmx

7) You may need to make the following change to the CodeDeploy.cmd script provided by Reza Alirezaei .

    @SET GACUTIL="C:\Program Files\Microsoft SDKs\Windows\v6.0A\bin\gacutil.exe"



Code Samples

Following are the files I used for this project:

LiveIDHttpModule.cs - Adds users to SharePoint and saves built-in custom profile information to the WSS "User Information List" site collection. If you un-comment the call to UpdateUserProfileData() it will also save built-in custom profile information to the MOSS User Profile database keeping both profiles synchronized.

LiveIDMembershipProvider.cs - Stubbed Minimal Membership Provider.

LiveIDLogin.aspx - Simulates Windows Live login.

LiveIDLogin.aspx.cs - Simulates Windows Live login.

AddWSSCustomProfileProperties.cs - Addes WSS custom profile properties.


//---------------------------------------------------------------------
//LiveIDHttpModule.cs:
//---------------------------------------------------------------------
Code:
using System;
using System.Configuration;
using System.Security;
using System.Security.Principal;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Reflection;
using System.Text.RegularExpressions;
using System.IO;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using Microsoft.Office.Server;
using Microsoft.Office.Server.UserProfiles;
using LiveIDHttpModule.ws_UserGroup;
using LiveIDHttpModule.ws_UserProfileService;

namespace LiveIDHttpModule
{
    public class LiveIDModule : IHttpModule
    {
        public virtual void Init(HttpApplication app)
        {
            // Subscribe to events.
            app.AuthenticateRequest += new EventHandler(app_AuthenticateRequest);

            app.PostAuthenticateRequest += new EventHandler(app_PostAuthenticateRequest);

            app.PreRequestHandlerExecute += new EventHandler(app_PreRequestHandlerExecute);
            //app.PostRequestHandlerExecute += new EventHandler(app_PreRequestHandlerExecute);

        }

        void app_PostAuthenticateRequest(object sender, EventArgs e)
        {
            if (HttpContext.Current.User.Identity.IsAuthenticated == true)
            {
                this.AddUser();
            }
        }

        void app_AuthenticateRequest(object sender, EventArgs e)
        {
            OnAuthenticateRequest(((HttpApplication)sender).Context);
        }

        void app_PreRequestHandlerExecute(object sender, EventArgs e)
        {
            HttpContext context = HttpContext.Current;

            //If the user clicked the "Sign in as a different user" or "Sign Out" menu options
            if (context.Request.Path.ToLower().Contains("/_layouts/signout.aspx") ||
                context.Request.Path.ToLower().Contains("/_layouts/accessdenied.aspx") ||
                context.Request.Url.PathAndQuery.ToLower().Contains("/_layouts/accessdenied.aspx?loginasanotheruser=true") ||
                context.Request.Path.ToLower().Contains("/_layouts/signout.aspx"))
            {

                // Code to remove FormsAuthentication cookie
                FormsAuthentication.SignOut();

                // Code to remove cookie name
                string cookieName = FormsAuthentication.FormsCookieName;
                HttpCookie authCookie = context.Request.Cookies[cookieName];
                authCookie.Expires = DateTime.Now.AddDays(-1D);
                context.Response.Cookies.Add(authCookie);

                // NOTE: The FormsAuthentication cookie is not being cleared. More work on this is needed.

                string requestedUrl = "http://" + context.Request.Url.Host + "/default.aspx";
                context.Response.Redirect(ConfigurationManager.AppSettings["LiveIDLoginSite"] + "/LiveIDLogin.aspx?returnurl=" + requestedUrl);
            }
        }

        public virtual void Dispose()
        {
        }

        /// 
        /// Authenticates the authorization request.
        /// 
        private void OnAuthenticateRequest(HttpContext context)
        {
            if (context == null)
                throw new ArgumentNullException("context");

            //Extract the forms authentication cookie
            string cookieName = FormsAuthentication.FormsCookieName;
            HttpCookie authCookie = context.Request.Cookies[cookieName];

            if (null == authCookie)
            {
                string requestedUrl = Uri.UnescapeDataString(context.Request.Url.ToString());

                //Simulate LiveID Login
                context.Response.Redirect(ConfigurationManager.AppSettings["LiveIDLoginSite"] + "/LiveIDLogin.aspx?returnurl=" + requestedUrl);
            }
        }

        private void SignOut()
        {
            if (HttpContext.Current.User.Identity.IsAuthenticated == true)
            {
                string cookieName = FormsAuthentication.FormsCookieName;
                HttpCookie authCookie = HttpContext.Current.Request.Cookies[cookieName];
                authCookie.Expires = DateTime.Now.AddDays(-1D);
                HttpContext.Current.Response.Cookies.Add(authCookie);
                FormsAuthentication.SignOut();
                FormsAuthentication.RedirectToLoginPage();
            }
        }

        private void AddUser()
        {
            //Extract the forms authentication cookie
            string cookieName = FormsAuthentication.FormsCookieName;
            HttpCookie authCookie = HttpContext.Current.Request.Cookies[cookieName];

            if (null != authCookie)
            {
                FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(authCookie.Value);
                string PUID = ticket.Name;
                string sUserData = ticket.UserData;
                string[] arrUserData = sUserData.Split("|".ToCharArray());
                string LastName = arrUserData[0];
                string FirstName = arrUserData[1];
                string WorkEmail = arrUserData[2];
                string Site = arrUserData[3];
                string SiteName = arrUserData[4];
                string ReportingRegion = arrUserData[5];

                string groupName = ConfigurationManager.AppSettings["LiveIDUserGroup"];
                string displayName = FirstName + " " + LastName;
                string userName = PUID;
                string sMembershipProviderName = "LiveID:";
                string userLoginName = sMembershipProviderName + PUID;
                string userNotes = "";

                AddUserToGroup(PUID, groupName, userLoginName, userName, displayName, WorkEmail, userNotes, FirstName, LastName, Site, SiteName, ReportingRegion);
            }
        }

        /// 
        /// Adds a user to a group
        /// 
        public void AddUserToGroup(string PUID, string groupName, string userLoginName, string userName, string displayName, string workEmail, string userNotes, string FirstName, string LastName, string Site, string SiteName, string ReportingRegion)
        {
            Boolean spSiteAllowUnsafeUpdate;
            Boolean spWebAllowUnsafeUpdate;
            SPSite spSite = null;
            SPWeb spWeb = null;

            SPSite currentSite = new SPSite(ConfigurationManager.AppSettings["LiveIDSPInternetZone"]);

            SPSecurity.RunWithElevatedPrivileges(delegate()
            {
                using (spSite = new SPSite(currentSite.ID))
                {
                    //using (SPWeb spWeb = spSite.RootWeb;
                    using (spWeb = spSite.OpenWeb("/"))
                    {
                        // Create WSS user in the "User Information List" site collection
                        // and add to the User Group "Live ID Visitors" (AppSettings["LiveIDUserGroup"])
                        AddUserToLiveIDGroup(groupName, userLoginName, userName, displayName, workEmail, userNotes);

                        // Add or Update MOSS User Profile database
                        // Include this if you want to use the MOSS User Profile database
                        //UpdateUserProfileData(userLoginName, userName, displayName, FirstName, LastName, workEmail, Site, SiteName, ReportingRegion);
                    }
                }
            });

            spSiteAllowUnsafeUpdate = spSite.AllowUnsafeUpdates;
            spWebAllowUnsafeUpdate = spWeb.AllowUnsafeUpdates;
            spSite.AllowUnsafeUpdates = true;
            spWeb.AllowUnsafeUpdates = true;
            spWeb.Update();

            try
            {
                //Update "User Information List" site collection
                UpdateUserInformationList(spWeb, userLoginName, userName, displayName, FirstName, LastName, workEmail, Site, SiteName, ReportingRegion);

                //I don't use GetUserProfile() but I include it as an example for using UserProfileManager()
                //UserProfile userProfile = GetUserProfile(spWeb, userLoginName);
            }
            finally
            {
                spWeb.AllowUnsafeUpdates = spWebAllowUnsafeUpdate;
                spSite.AllowUnsafeUpdates = spSiteAllowUnsafeUpdate;
            }
        }

        /// 
        /// Update "User Information List" site collection
        /// 
        private void UpdateUserInformationList(SPWeb spWeb, string userLoginName, string userName, string displayName, string FirstName, string LastName, string WorkEmail, string Site, string SiteName, string ReportingRegion)
        {
            // Get the current user from AllUsers "User Information List" site collection
            SPUser user = spWeb.AllUsers[userLoginName];

            // Update the display Name and work Email Properties in the "User Information List" site collection
            user.Name = displayName;
            user.Email = WorkEmail;
            user.Update();

            // The other user information is in SPListItem
            // spWeb.SiteUserInfoList.GetItemById(some_id) is about 5 time faster
            // than spWeb.SiteUserInfoList.Items[some_id]
            SPList siteUserInfoList = spWeb.SiteUserInfoList;
            SPListItem userItem = siteUserInfoList.GetItemById(user.ID);

            // "User Information List" site collection built-in WSS items
            // Title, Name, EMail, Notes, SipAddress, Locale, IsSiteAdmin, Picture, Department, JobTitle, IsActive,
            // FirstName, LastName, WorkPhone, UserName, WebSite, Office, ID, Modified, Created, Author, Editor

            // Note: Following values are set by UserGroup.AddUserToGroup():
            // userItem["Name"] is the Account login name
            // userItem["Title"] is the display name and the user.Name
            // userItem["EMail"] is the Work Email and the user.Email
            // userItem["Notes"] is the userNotes
            // userItem["Modified"] is handled by SharePoint
            // Note: If the user is deleted and re-created days later, the ["Created"] field remains the date first created.
            // userItem["Created"] is handled by SharePoint

            userItem["UserName"] = userName;
            userItem["Department"] = "OPC Partner";
            userItem["FirstName"] = FirstName;
            userItem["LastName"] = LastName;

            //Custom properties were created in SSPAdmin "User Profiles and Properties/Add Profile Properties"
            //userItem["SiteID"] = Site;
            //userItem["SiteName"] = SiteName;
            //userItem["Region"] = ReportingRegion;

            userItem.Update();

        }

        /// 
        /// Create WSS user in the "User Information List" site collection
        /// and add to the User Group "Live ID Visitors" (AppSettings["LiveIDUserGroup"])
        /// 
        public void AddUserToLiveIDGroup(string groupName, string userLoginName, string userName, string displayName, string userEmail, string userNotes)
        {
            // Add user to SharePoint.
            // groupName        A string that contains the name of the cross-site group.
            // userName         A string that contains the display name of the user.
            // userLoginName    A string that contains the user name (DOMAIN\User_Alias) of the user.
            // userEmail        A string that contains the e-mail address of the user.
            // userNotes        A string that contains notes for the user.

            // Note how data is added to the following WSS "User Information List" properties:
            // Account = userLoginName (Ex: liveid:00034oiaso3566wa)
            // Name = displayName (Ex: Joe Doe)

            // Note: After the user is added to the MOSS User Profile database using CreateUserProfileByAccountName()
            // the following properties are eventually synchronized between
            // the MOSS User Profile database and the WSS "User Information List":
            // Username = 00034OIASO3566WA
            // AccountName = LiveID:00034OIASO3566WA

            ws_UserGroup.UserGroup UserGroup = new UserGroup();
            UserGroup.Credentials = System.Net.CredentialCache.DefaultCredentials;
            UserGroup.AddUserToGroup(groupName, displayName, userLoginName, userEmail, userNotes);
        }

        /// 
        /// Add or Update MOSS User Profile database
        /// 
        private void UpdateUserProfileData(string userLoginName, string userName, string displayName, string FirstName, string LastName, string WorkEmail, string Site, string SiteName, string ReportingRegion)
        {
            int totalProperties;

            // Create an instance of the Web service proxy class.
            ws_UserProfileService.UserProfileService service = new UserProfileService();

            // Specify the network credentials of the current
            // security context. For the profile creation to
            // succeed the credentials must be for an account
            // with permissions to create user profiles.
            service.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
            service.PreAuthenticate = true;

            // Check whether the user already has a profile.
            ws_UserProfileService.PropertyData[] propertyData = null;

            try
            {
                // Check if user exists
                propertyData = service.GetUserProfileByName(userLoginName);
            }
            catch { }

            // If user exists
            if (propertyData != null)
            {
                totalProperties = 9;
            }
            // If user does not exist
            else
            {
                // Add User to the MOSS User Profile database.
                // Following properties are updated:
                // AccountName = LiveID:00034OIASO3566WA
                // UserName = 00034OIASO3566WA

                // Note: After the user is added to the MOSS User Profile database using CreateUserProfileByAccountName()
                // the following properties are eventually synchronized between
                // the MOSS User Profile database and the WSS "User Information List":
                // Username = 00034OIASO3566WA
                // AccountName = LiveID:00034OIASO3566WA

                service.CreateUserProfileByAccountName(userLoginName);
                totalProperties = 10;
            }

            // Set some of the profile properties.
            ws_UserProfileService.PropertyData[] newdata = new ws_UserProfileService.PropertyData[totalProperties];

            //Available built-in MOSS properties
            //AboutMe, AccountName, ADGuid, Assistant, Birthday, CellPhone, Department, DirectReports,
            //DontSuggestList, Dottedline, Fax, FirstName, HireDate, HomePhone, Interests,
            //LastColleagueAdded, LastName, Manager, MasterAccountName, MySiteUpgrade, Office,
            //OutlookWebAccessUrl, PastProjects, Peers, PersonalSpace, PictureUrl, PreferredName,
            //PublicSiteRedirect, QuickLinks, ResourceAccountName, ResourceSID, Responsibility,
            //School, SID, SipAddress, Skills, Title, UserGuid, UserName, WebSite, WorkEmail, WorkPhone

            // Set the Name property.
            newdata[0] = new ws_UserProfileService.PropertyData();
            newdata[0].Name = PropertyConstants.PreferredName;
            newdata[0].Values = new ws_UserProfileService.ValueData[1];
            newdata[0].Values[0] = new ws_UserProfileService.ValueData();
            newdata[0].Values[0].Value = displayName;
            newdata[0].IsValueChanged = true;

            // Set the Department property.
            newdata[1] = new ws_UserProfileService.PropertyData();
            newdata[1].Name = PropertyConstants.Department;
            newdata[1].Values = new ws_UserProfileService.ValueData[1];
            newdata[1].Values[0] = new ws_UserProfileService.ValueData();
            newdata[1].Values[0].Value = "OPC Partner";
            newdata[1].IsValueChanged = true;

            // Set the FirstName property.
            newdata[2] = new ws_UserProfileService.PropertyData();
            newdata[2].Name = PropertyConstants.FirstName;
            newdata[2].Values = new ws_UserProfileService.ValueData[1];
            newdata[2].Values[0] = new ws_UserProfileService.ValueData();
            newdata[2].Values[0].Value = FirstName;
            newdata[2].IsValueChanged = true;

            // Set the LastName property.
            newdata[3] = new ws_UserProfileService.PropertyData();
            newdata[3].Name = PropertyConstants.LastName;
            newdata[3].Values = new ws_UserProfileService.ValueData[1];
            newdata[3].Values[0] = new ws_UserProfileService.ValueData();
            newdata[3].Values[0].Value = LastName;
            newdata[3].IsValueChanged = true;

            // Set the WorkEmail property.
            newdata[4] = new ws_UserProfileService.PropertyData();
            newdata[4].Name = PropertyConstants.WorkEmail;
            newdata[4].Values = new ws_UserProfileService.ValueData[1];
            newdata[4].Values[0] = new ws_UserProfileService.ValueData();
            newdata[4].Values[0].Value = WorkEmail;
            newdata[4].IsValueChanged = true;

            //Custom properties were created in SSPAdmin "User Profiles and Properties/Add Profile Properties"
            //Available Custom properties
            //SiteID, SiteName, Region, Created, Modified

            // Set the Site property.
            newdata[5] = new ws_UserProfileService.PropertyData();
            newdata[5].Name = "SiteID";
            newdata[5].Values = new ws_UserProfileService.ValueData[1];
            newdata[5].Values[0] = new ws_UserProfileService.ValueData();
            newdata[5].Values[0].Value = Site;
            newdata[5].IsValueChanged = true;

            // Set the SiteName property.
            newdata[6] = new ws_UserProfileService.PropertyData();
            newdata[6].Name = "SiteName";
            newdata[6].Values = new ws_UserProfileService.ValueData[1];
            newdata[6].Values[0] = new ws_UserProfileService.ValueData();
            newdata[6].Values[0].Value = SiteName;
            newdata[6].IsValueChanged = true;

            // Set the ReportingRegion property.
            newdata[7] = new ws_UserProfileService.PropertyData();
            newdata[7].Name = "Region";
            newdata[7].Values = new ws_UserProfileService.ValueData[1];
            newdata[7].Values[0] = new ws_UserProfileService.ValueData();
            newdata[7].Values[0].Value = ReportingRegion;
            newdata[7].IsValueChanged = true;

            // Set the ReportingRegion property.
            newdata[8] = new ws_UserProfileService.PropertyData();
            newdata[8].Name = "Modified";
            newdata[8].Values = new ws_UserProfileService.ValueData[1];
            newdata[8].Values[0] = new ws_UserProfileService.ValueData();
            newdata[8].Values[0].Value = DateTime.Now;
            newdata[8].IsValueChanged = true;

            if (totalProperties == 10)
            {
                // Set the ReportingRegion property.
                newdata[9] = new ws_UserProfileService.PropertyData();
                newdata[9].Name = "Created";
                newdata[9].Values = new ws_UserProfileService.ValueData[1];
                newdata[9].Values[0] = new ws_UserProfileService.ValueData();
                newdata[9].Values[0].Value = DateTime.Now;
                newdata[9].IsValueChanged = true;
            }

            // Update the user profile.
            service.ModifyUserPropertyByAccountName(userLoginName, newdata);
        }

        /// 
        /// Get User Profile from UserProfileManager
        /// 
        public UserProfile GetUserProfile(SPWeb spWeb, string LoginName)
        {
            ServerContext context = ServerContext.GetContext(spWeb.Site);
            UserProfileManager upMgr = new UserProfileManager(context);
            UserProfile profile = upMgr.GetUserProfile(LoginName);
            return profile;
        }

    }
}


//---------------------------------------------------------------------
//LiveIDMembershipProvider.cs:
//---------------------------------------------------------------------

Code:
/**
* This code is provided "AS IS", without warranty of any kind.
* This is a sample code, might be not completely error free.
*/
using System;
using System.Collections.Generic;
using System.Text;
using System.Web.Security;
using System.Collections.Specialized;
using System.Web;

namespace LiveIDHttpModule
{
    public class LiveIDMembershipProvider : MembershipProvider
    {
        #region Private Member Variables
        private string _ApplicationName;
        private string _ProviderName;
        #endregion

        #region Property Overrides

        /// <summary>
        /// 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.
        /// </summary>
        public override string ApplicationName
        {
            get
            {

                return _ApplicationName;
            }
            set
            {
                _ApplicationName = value;
            }
        }

        /// <summary>
        ///
        /// </summary>
        public override string Name
        {
            get
            {
                return _ProviderName;
            }
        }
        #endregion

        #region  Method Overrides

        /// <summary>
        /// Passes on the values of the configuration settings for the provider to the code at runtime
        /// </summary>
        /// <param name="name">Name of Provider</param>
        /// <param name="config">Settings as name-value pairs in an instance of the NameValueCollection class</param>
        public override void Initialize(string name, NameValueCollection config)
        {

            // Check to see if config object is not null
            if (config == null)
                throw new ArgumentNullException("config");

            //Inisitialize Provider Name and Application Name
            _ProviderName = name;
            _ApplicationName = "/";

            // Initialize base class
            base.Initialize(name, config);

            //TODO: Implement Initialize method spcific for your authentication provider

        }

        /// <summary>
        /// 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.
        /// </summary>
        /// <param name="username">username</param>
        /// <param name="password">password</param>
        /// <returns></returns>
        public override bool ValidateUser(string username, string password)
        {
            //TODO: Implement ValidateUser method
            return false;
        }

        /// <summary>
        /// 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.
        /// </summary>
        /// <param name="username">user name</param>
        /// <param name="userIsOnline">a Boolean value indicating whether to update the user's LastActivityDate to show that the user is currently online</param>
        /// <returns></returns>
        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            //TODO: Implement GetUser method
            return null;
        }

        /// <summary>
        /// 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.
        /// </summary>
        /// <param name="email"></param>
        /// <returns>first registered user name whose e-mail address matches the one supplied as email param</returns>
        public override string GetUserNameByEmail(string email)
        {
            //TODO: Implement GetUserNameByEmail method
            return null;
        }

        /// <summary>
        /// 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.
        /// </summary>
        /// <param name="pageIndex">Identifies which page of results to return. Page indexes are 0-based</param>
        /// <param name="pageSize">Specifies the maximum number of MembershipUser objects to return.</param>
        /// <param name="totalRecords">Holds a count of all registered users.</param>
        /// <returns></returns>
        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            //TODO: Implement GetAllUsers method
            totalRecords = 0;
            MembershipUserCollection muc = new MembershipUserCollection();
            return muc;
        }

        /// <summary>
        /// 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.
        /// </summary>
        /// <param name="emailToMatch">Specifies search criteria for email.</param>
        /// <param name="pageIndex">See the GetAllUsers method.</param>
        /// <param name="pageSize">See the GetAllUsers method.</param>
        /// <param name="totalRecords">See the GetAllUsers method.</param>
        /// <returns></returns>
        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            //TODO: Implement FindUsersByEmail method
            totalRecords = 0;
            MembershipUserCollection muc = new MembershipUserCollection();
            return muc;
        }

        /// <summary>
        /// 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.
        /// </summary>
        /// <param name="usernameToMatch">Specifies search criteria for username.</param>
        /// <param name="pageIndex">See the GetAllUsers method.</param>
        /// <param name="pageSize">See the GetAllUsers method.</param>
        /// <param name="totalRecords">See the GetAllUsers method.</param>
        /// <returns></returns>
        public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            //TODO: Implement FindUsersByName method
            totalRecords = 0;
            MembershipUserCollection muc = new MembershipUserCollection();
            return muc;
        }

        #endregion

        #region Property Overrides (Not Supported Yet)
        public override string Description
        {
            get
            {
                throw new NotSupportedException();
            }
        }
        public override bool EnablePasswordRetrieval
        {
            get { throw new NotSupportedException(); }
        }
        public override bool EnablePasswordReset
        {
            get { throw new NotSupportedException(); }
        }
        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(); }
        }
        #endregion

        #region Method Overrides (Not Supported Yet)
        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 MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            throw new NotSupportedException();
        }
        public override string GetPassword(string username, string answer)
        {
            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();
        }
        protected override byte[] DecryptPassword(byte[] encodedPassword)
        {
            throw new NotSupportedException();
        }
        protected override byte[] EncryptPassword(byte[] password)
        {
            throw new NotSupportedException();
        }
        protected override void OnValidatingPassword(ValidatePasswordEventArgs e)
        {
            throw new NotSupportedException();
        }
        #endregion

    }
}


Login Code Sample

I also include a LiveID Login page to simplify the need to use Live ID logins.

//---------------------------------------------------------------------
//LiveIDLogin.aspx:
//---------------------------------------------------------------------

Code:
<%@ Page Language="C#" AutoEventWireup="true"  CodeFile="LiveIDLogin.aspx.cs" Inherits="_Default" %>

<!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 runat="server">
    <title>LiveID Login Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
    <table>
        <tr>
            <td>

                <table style="width: 315px">
                    <tr>
                        <td>
                            PUID</td>
                        <td>
                            <asp:TextBox ID="TextBox1" runat="server" Width="295px">00034OIASO3566WA</asp:TextBox>
                        </td>
                    </tr>
                    <tr>
                        <td>
                            Lastname</td>
                        <td>
                            <asp:TextBox ID="TextBox2" runat="server" Width="295px">Doe</asp:TextBox>
                        </td>
                    </tr>
                    <tr>
                        <td>
                            Firstname</td>
                        <td>
                            <asp:TextBox ID="TextBox3" runat="server" Width="295px">Joe</asp:TextBox>
                        </td>
                    </tr>
                    <tr>
                        <td>
                            Email Address</td>
                        <td>
                            <asp:TextBox ID="TextBox4" runat="server" Width="295px">jdoe@test.com</asp:TextBox>
                        </td>
                    </tr>
                    <tr>
                        <td>
                            Site</td>
                        <td>
                            <asp:TextBox ID="TextBox5" runat="server" Width="295px">1</asp:TextBox>
                        </td>
                    </tr>
                    <tr>
                        <td>
                            Sitename</td>
                        <td>
                            <asp:TextBox ID="TextBox6" runat="server" Width="295px">northamerica</asp:TextBox>
                        </td>
                    </tr>
                    <tr>
                        <td>
                            Reporting Region</td>
                        <td>
                            <asp:TextBox ID="TextBox7" runat="server" Width="295px">USA</asp:TextBox>
                        </td>
                    </tr>
                    <tr>
                        <td>
                            Level2DomainName</td>
                        <td>
                            <asp:TextBox ID="TextBox8" runat="server" Width="295px">moss.local</asp:TextBox>
                        </td>
                    </tr>
                </table>

            </td>
        </tr>
        <tr>
            <td>
                User Identity:
                <asp:Label ID="Label1" runat="server"></asp:Label>
            </td>
            <td>
            &nbsp;</td>
        </tr>
        <tr>
            <td>
                <asp:Button ID="Button1" runat="server" onclick="Button1_Click"
                    Text="Login" />
            </td>
        </tr>
        <tr>
            <td>
                <asp:TextBox ID="MultiLine" Visible=false runat="server" Height="229px" Width="717px"
                    TextMode="MultiLine"></asp:TextBox>
            </td>
        </tr>
    </table>
    </div>
    </form>
</body>
</html>


//---------------------------------------------------------------------
//LiveIDLogin.aspx.cs:
//---------------------------------------------------------------------

Code:
using System;
using System.Configuration;
using System.Data;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;

public partial class _Default : System.Web.UI.Page
{
    string sReturnURL;

    protected void Page_Load(object sender, EventArgs e)
    {
        Label1.Text = User.Identity.Name;
    }

    protected void  Button1_Click(object sender, EventArgs e)
    {

        sReturnURL = HttpContext.Current.Request.QueryString["returnurl"];
        if ((sReturnURL == null))
        {
            sReturnURL = "";
        }

        //oUser.LastName, oUser.FirstName, oUser.EmailAddress, oUser.Site.ToString, oSite.SiteName ,oSite.ReportingRegion
        string sUserData = TextBox2.Text + "" + TextBox3.Text + "" + TextBox4.Text + "" + TextBox5.Text + "" + TextBox6.Text + "" + TextBox7.Text;

        //Call SetAuthCookie method to log in. A cookie is created.
        //Domain name in the cookie defaults to the subdomain where the application resides
        //FormsAuthentication.SetAuthCookie(oUser.PUID, False)

        //Modify the Domain attribute of the cookie to the second level of domain
        System.Web.HttpCookie Cookie = FormsAuthentication.GetAuthCookie(TextBox1.Text, false);

        //The FormsAuthentication class does not provide any methods for specifying UserData information
        //in its GetAuthCookie() and SetAuthCookie() methods so this addes the UserData to the FormsAuthentication cookie

        FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(Cookie.Value);

        // Store user data inside the Forms cookie.
        FormsAuthenticationTicket newticket = new FormsAuthenticationTicket(ticket.Version, ticket.Name, ticket.IssueDate, ticket.Expiration, ticket.IsPersistent, sUserData, ticket.CookiePath);

        //Set level 2 domain
        Cookie.Domain = TextBox8.Text;

        //Used only with SetAuthCookie()
        //Context.Response.AppendCookie(Cookie)

        Cookie.Value = FormsAuthentication.Encrypt(newticket);
        Context.Response.Cookies.Set(Cookie);

        //If ReturnURL redirect missing
        if (sReturnURL.Length == 0)
        {
            //Try getting redirect URL from FormsAuthentication
            sReturnURL = System.Web.Security.FormsAuthentication.GetRedirectUrl(TextBox1.Text, false);
        }

        //If ReturnURL redirect
        if (sReturnURL.Length > 0)
        {
            Context.Response.Redirect(sReturnURL);
            //HttpContext.Current.Response.Redirect(HttpContext.Current.Request.Url.ToString());
        }
        else
        {
            throw new Exception("Login called without \"ReturnURL\" parameter in the querystring.");
        }
    }
}


//---------------------------------------------------------------------
//AddWSSCustomProfileProperties.cs:
//---------------------------------------------------------------------

Code:
using System;
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Office.Server;
using Microsoft.Office.Server.Administration;
using Microsoft.Office.Server.UserProfiles;
using Microsoft.SharePoint;
using System.Web;

namespace AddWSSCustomProfileProperties
{
    class Program
    {
        static void Main(string[] args)
        {
            //Adds Custom WSS Profile Properties.
            using (SPSite spSite = new SPSite("http://liveidsp.moss.local"))
            {
                ServerContext context = ServerContext.GetContext(spSite);
                UserProfileManager profileManager = new UserProfileManager(context);
                try
                {
                    //Add the properties
                    PropertyCollection pc = profileManager.Properties;
                    Property p = pc.Create(false);
                    p.Name = "SiteID";
                    p.DisplayName = "SiteID";
                    p.Type = PropertyDataType.Integer;
                    p.PrivacyPolicy = PrivacyPolicy.OptIn;
                    p.DefaultPrivacy = Privacy.Public;
                    pc.Add(p);

                    p.Name = "SiteName";
                    p.DisplayName = "SiteName";
                    p.Type = PropertyDataType.String;
                    p.Length = 80;
                    p.PrivacyPolicy = PrivacyPolicy.OptIn;
                    p.DefaultPrivacy = Privacy.Public;
                    pc.Add(p);

                    p.Name = "Region";
                    p.DisplayName = "Region";
                    p.Type = PropertyDataType.String;
                    p.Length = 80;
                    p.PrivacyPolicy = PrivacyPolicy.OptIn;
                    p.DefaultPrivacy = Privacy.Public;
                    pc.Add(p);
                }
                catch (DuplicateEntryException e)
                {
                    Console.WriteLine(e.Message);
                    Console.Read();
                }
                catch (System.Exception e2)
                {
                    Console.WriteLine(e2.Message);
                    Console.Read();
                }
            }
        }
    }
}

2 comments:

Amit Shakya said...

Hey Burt,

I am a learner in MOSS 2007.I have created the FBA but not able to integrate with my existing site.Moreover I have stuck with dynamic database updation.Your article can surely solve my problem.

Please tell, Where to apply this code? It will be very appreciated.My gmail id is amitshakya90@gmail.com.Looking forward for your reply.Thanks a in advance.

With regards
Amit Shakya

bjohnson said...

Amit, I'm surprised your still on SharePoint 2007. I wrote this so long ago I had to re-read it just to remember what it was about.
This was designed for cross domains host access with one host used to login using FDA and the other host to update SharePoint user profiles. Its a high level discussion that assumes you understand SharePoint Forms Authentication (FBA) and Authentication Provider services.
You should start by reading the article "Guest Account Enabler" by Reza Alirezaei (Link provided above) that explains in more detail how to setup a Guest Membership Provider service. After you get that working, this code and the Implementation instructions will hopefully make more sense. Let me know if you have further question after you attempt the "Guest Account Enabler". Thanks