Creating Forms-Based Authentication and User Profiles in SharePoint 2013 using Custom Membership and Role Providers and a Custom User Profile Synchronization Utility


Created: July 2012
Summary: How to create forms-based authentication and user profiles
Applies to: Microsoft SharePoint Server 2013 (Beta 2)
Provided by: Benedikt Redl

Overview

This blog shows how you can create a custom membership and role provider for a forms-based web application and how you can synchronize the forms-based user profiles with the user profile service application.

Please keep in mind that the code is for demonstration purpose only and is no production code. Even changing web.config files manually should only be used in evaluation environments.

Step1: Membership and Role Provider

The implementation of the membership and role provider in SharePoint 2013 is pretty much the same as in SharePoint 2010 except the newer .Net Framework, SP-API and Visual Studio 2012.

(you can also follow the steps in http://msdn.microsoft.com/en-us/library/gg317440.aspx described for SharePoint 2010, but the code for this walkthrough is missing, therefore I re-implemented the C# code)

To create the SP Solution open Visual Studio 2012 and create a new SharePoint 2013 Project:
 image

  In my sample its name is FBAClaimsProvider.

Add a feature and give it Farm scope:
image 

Add three class files Members.cs, Roles.cs and UserData.cs

The project has the following references:
 image

Code for the class files:

1. The UserData.cs contains the code to simulate the user database

public class UserData
    {
        //loginName:email:givenname:lastname:aboutme:department
        public static string[] UserDB = {
        "user1:user1@contoso.com:Givenname1:Lastname1:My expertise is to be user1:Department1",
        "user2:user2@contoso.com:Givenname2:Lastname2:My expertise is to be user2:Department2",
        "user3:user3@contoso.com:Givenname3:Lastname3:My expertise is to be user3:Department3",
        "user4:user4@contoso.com:Givenname4:Lastname4:My expertise is to be user4:Department4",
        "user5:user5@contoso.com:Givenname5:Lastname5:My expertise is to be user5:Department5",
        "user6:user6@contoso.com:Givenname6:Lastname6:My expertise is to be user6:Department6"
        };

        public static string[] UserRoleDB = {
        "user1:Role1:Role2:Role3",
        "user2:Role2:Role4",
        "user3:Role3:Role1:Role4",
        "user4:Role4:Role1:Role2",
        "user5:Role2:Role1",
        "user6:Role1:Role4"
        };

        public static string[] RoleDB = {"Role1", "Role2", "Role3", "Role4"};
    }

UserDB is a string array that simulates the user table, UserRoleDB the roles-per-user table and RoleDB the table with available roles (columns are separated by a ‘:’).

2. The membership provider is implemented in Members.cs:

using System.Text;
using System.Threading.Tasks;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration.Claims;
using System.Web.Security;
using System.Diagnostics;

namespace FBAClaimsProvider
{
    public class Members : System.Web.Security.MembershipProvider
    {

The Members class derives from System.Web.Security.MembershipProvider

Please implement all methods with
image

The GetUser() methods are used to get the MemberShipUser object that is bases on the user login name. Change the two methods to:

public static string ProviderName = "FBAMembershipProvider";


public override MembershipUser GetUser(string username, bool userIsOnline)
{
    MembershipUser mu = null;
    for (int i = 0; i < UserData.UserDB.Count(); i++)
    {
        string userEntry = UserData.UserDB[i];
        string[] userAttribs = userEntry.Split(':');
        if (username == userAttribs[0])
        {
            mu = new MembershipUser(ProviderName, userAttribs[0], i, userAttribs[1], "", "", true,
                         false, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now);
            break;
        }
    }
    return mu;
}
public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
{
    int i = Convert.ToInt32(providerUserKey);
    string userEntry = UserData.UserDB[i];
    MembershipUser mu = null;
    string[] userAttribs = userEntry.Split(':');
    mu = new MembershipUser(ProviderName, userAttribs[0], i, userAttribs[1], "", "", true, 
                 false, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now);
    return mu;
}

The FindUserByEmail() and FindUserByName() methods are called by the People Picker when the user tries to search or resolve user names:

public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
{
        MembershipUserCollection muc = new MembershipUserCollection();
        totalRecords = 0;
        for (int i = 0; i < UserData.UserDB.Count(); i++)
        {
            string userEntry = UserData.UserDB[i];
            string[] userAttribs = userEntry.Split(':');
            if (userAttribs[1].ToLower().IndexOf(emailToMatch.ToLower()) > -1)
            {
                muc.Add(new MembershipUser(ProviderName, userAttribs[0], i, userAttribs[1], "", "", true, false, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now));
                totalRecords++;
            }
        }

        return muc;
}
public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
{
            

    MembershipUserCollection muc = new MembershipUserCollection();
    totalRecords = 0;
    for (int i = 0; i < UserData.UserDB.Count(); i++)
    {
        string userEntry = UserData.UserDB[i];
        string[] userAttribs = userEntry.Split(':');
        if (userAttribs[0].ToLower().IndexOf(usernameToMatch.ToLower()) > -1)
        {
            muc.Add(new MembershipUser(ProviderName, userAttribs[0], i, userAttribs[1], "", "", true, false, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now));
            totalRecords++;
        }
    }
    return muc;
}

Implement the GetAllUsers() and ValidateUser() methods:

public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
{
    MembershipUserCollection muc = new MembershipUserCollection();
    totalRecords = 0;
    for (int i = 0; i < UserData.UserDB.Count(); i++)
    {
        string userEntry = UserData.UserDB[i];
        string[] userAttribs = userEntry.Split(':');
        muc.Add(new MembershipUser(ProviderName, userAttribs[0], i, userAttribs[1], "", "", true, false, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now));
        totalRecords++;
    }
    return muc;
}

public override bool ValidateUser(string username, string password)
{
    for (int i = 0; i < UserData.UserDB.Count(); i++)
    {
        string userEntry = UserData.UserDB[i];
        string[] userAttribs = userEntry.Split(':');
        if (username == userAttribs[0])
        {
            // todo: in production validate password ...

            return true;
        }
    }
    return false;
}

The ValidateUser() methods validates the user’s credentials. In this sample the password is not verified.

3. The role provider is implemented in Roles.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration.Claims;
using System.Web.Security;

namespace FBAClaimsProvider
{

    public class Roles : System.Web.Security.RoleProvider
    {

The Roles class derives from System.Web.Security.RoleProvider

Please implement all methods with
image

Implement the GetRolesForUser() and RoleExists() methods

public override string[] GetRolesForUser(string username)
{
    string[] roles = new string[]{};

    for (int i = 0; i < UserData.UserRoleDB.Count(); i++)
    {
        string userEntry = UserData.UserRoleDB[i];
        string[] userAttribs = userEntry.Split(':');
        if (username == userAttribs[0] && userAttribs.Count()>1)
        {
            roles = userEntry.Substring(userEntry.IndexOf(':') + 1).Split(':');
            break;
        }
    }

    return roles;
}

public override bool RoleExists(string roleName)
{
    foreach (string role in UserData.RoleDB)
    {
        if (role == roleName)
            return true;
    }

    return false;
}

The GetRolesForUsers() method is used during the logon process to obtain the user’s claim information.

Build and deploy the project.

 

Step 2: Creating a SharePoint FBA Web Application

To Create a SharePoint FBA Web Application go to Central Administration,

Application Management, Manage web applications, New

Fill out the necessary fields.

Enable Forms Based Authentication:
image

Select Default Sign In Page:
image

Create a Site Collection for testing.

Step 3: Configuring the Membership and Role Provider

To configure the membership and role providers the web.config of the following must be modified:

  • Central Administration to enable picking
  • Security Token Service to enable sign in and for issuing tokens
  • FBA Web Application to enable picking
  • My Site Host to enable picking

I STRONGLY recommend to make a backup of your central admin, STS, and web application and my site host web.config files before pushing out changes.

Step 4: Testing the Forms-Based Authentication

Go to your FBA Site (I have configured Windows and Forms Authentication for my Web App, so SP prompts to choose the authentication method):
image

Choose Forms Authentication
image

and input a user (I didn’t implement password validation in the membership provider, so I need not input a password). Click Sign In:
image

If you get any errors check your web.config modifications (most causes for errors).

Step 5: User Profile Synchronization Utility

SharePoint does not provide a OOB possibility to synchronize your own user profile data sources. Like in SharePoint 2010 only AD variants and three Ldap-Directories are supported. BCS can only be used for property augmentation – not to create User Profiles. But you can implement your own user profile synchronization using code.

This sample shows a Windows Console application utility to import user profiles. As precondition the UPA (User Profile Service Application) must be configured and running.

The most important thing when importing user profiles with code is to use the correct user account names . As you use FBA claims, the format is:

i:0#.f|fbamembershipprovider|<user-login-name>

You can crosscheck this through taking a look into the UserInfo-Table of your FBA Web Site:
image

This entry was created during the first login of your login test.

To implement the utility create a Windows console applications (be sure to change the platform target – it must be a 64bit app).

The sample uses the UserDB-array implemented in the FBAClaimsProvider to ensure a common data source

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.SharePoint;
using Microsoft.Office.Server.UserProfiles;
using FBAClaimsProvider;

namespace ImportUserProfiles
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                //claim-id: 
                //identity claim: i
                //Username #
                //string .
                //forms authn f
                //formsprovider nucleus
                //userlogin
                string claimsPrefix = &quot;i:0#.f|fbamembershipprovider|&quot;; 

                using (SPSite site = new SPSite(&quot;http://fba.contoso.com&quot;))
                {
                    Microsoft.SharePoint.SPServiceContext context = SPServiceContext.GetContext(site);
                    UserProfileManager upm = new UserProfileManager(context,true,true);
                    ProfileSubtypeManager psm = ProfileSubtypeManager.Get(context);

                    // choose default user profile subtype as the subtype
                    string subtypeName = ProfileSubtypeManager.GetDefaultProfileName(ProfileType.User);
                    ProfileSubtype subType = psm.GetProfileSubtype(subtypeName);
                    

                    string[] userlist = UserData.UserDB;

                    foreach (string userEntry in userlist)
                    {
                        try
                        {
                            string[] userAttribs = userEntry.Split(':'); //loginName:email:givenname:lastname:aboutme:department
                            string accountName = claimsPrefix + userAttribs[0];
                            string email = userAttribs[1];
                            string givenname = userAttribs[2];
                            string lastname = userAttribs[3];
                            string aboutme = userAttribs[4];
                            string department = userAttribs[5];

                            UserProfile profile;
                            if (upm.UserExists(accountName))
                            {
                                //todo: update user profile and/or write changes back to user db
                            }
                            else
                            {
                                // create a user profile and set properties
                                profile = upm.CreateUserProfile(accountName);
                                profile.DisplayName = givenname + &quot; &quot; + lastname;
                                profile[PropertyConstants.WorkEmail].Value = email;

                                profile[PropertyConstants.FirstName].Value = givenname;
                                profile[PropertyConstants.LastName].Value = lastname;
                                profile[PropertyConstants.AboutMe].Value = aboutme;
                                profile[PropertyConstants.Department].Value = department;
                                profile.ProfileSubtype = subType;
                                profile.Commit();
                            }
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine(&quot;User &quot; + userEntry + &quot; exception &quot; + ex.ToString());
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
        }
    }
}

 

Note: you must run the import-utility under the account you configured as pool account for the UPA. I my case this account is the farm-account. Otherwise you get the following exception:

Microsoft.Office.Server.UserProfiles.UserProfileApplicationNotAvailableException: UserProfileApplicationNotAvailableException_Logging :: UserProfileApplicationProxy.ApplicationProperties ProfilePropertyCache does not have ae77f06a-b57f-4020-a93b-eeff01c2efb4

Additional Resources

Configure forms-based authentication for a claims-based Web application (SharePoint Foundation 2010)
http://technet.microsoft.com/en-us/library/ee806882(office.14).aspx

FBA Configuration Manager for SharePoint 2013
http://blogs.technet.com/b/speschka/archive/2012/07/28/fba-configuration-manager-for-sharepoint-2013.aspx

Downloads:
FBA Web.config-Modifications.txt,
FBA Solutionfiles.zip

Advertisements
This entry was posted in SharePoint Server 2013, User Profiles. Bookmark the permalink.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s