Creating a SharePoint Custom Claim Provider to Enhance the SharePoint People Picker

This article describes how to build a custom claim provider that allows to the picking of active directory people and groups in a multi-domain environment.

The source code for this is locatedhere

Before we are able to use this custom claim provider, we must modify a few values to suit a specific organization. Let’s look at these default values…

internalconststring ClaimProviderDisplayName = "Custom";

internalconststring ClaimProviderDescription = "Custom Claim Provider from Learn.SharePoint.com";

conststring SPTrustedIdentityTokenIssuerName = "Custom Identity Source";

conststring userLDAPUniqueIdentifier = "mail";

conststring usersClaim = "Users";

conststring groupsClaim = "Groups";

conststring forestDN = "DC=contoso,DC=com";

conststring forestName = "contoso";

conststring corporateDomainName = "contoso";

My organization’s name is Catholic Health Initiatives. We have multiple domains inside of a single forest. Our forest distinguished name is DC=catholichealth,DC=net, and most of our users are stored inside of a domain called CHI. For us, the following configuration is used…

internalconststring ClaimProviderDisplayName = "CHI Directory";

internalconststring ClaimProviderDescription = "Claims provider that searches Users/Groups in CHI's Enterprise Active Directory";

conststring SPTrustedIdentityTokenIssuerName = "Catholic Health Federation Services";

conststring userLDAPUniqueIdentifier = "mail";

conststring usersClaim = "Users";

conststring groupsClaim = "Groups";

conststring forestDN = "DC=catholichealth,DC=net";

conststring forestName = "catholichealth.net";

conststring corporateDomainName = "chi";

The ClaimProviderDisplayName is the value shown in the popup window left treeview, as well as after search results in the people editor control to disambiguate results. Figure 3is screenshots of the popup window and the people editor disambiguation.

Figure 3 – ClaimProviderName Appearing in the Popup Window and the People Editor

The ClaimProviderDescriptionappears in powershell

Figure 4 – PowerShell Showing the Description Value

The SPTrustedIdentityTokenIssuerName is used to build the full claim string. For example, my full claim string is i:05.t|catholic health federation services|(my work email address). For more information on the full claim string, check out

The other configuration values will be explained later.

Now let’s look at the two claim types

conststring IdentityClaimType = "

conststring GroupClaimType = "

It’s not uncommon to change the IdentityClaimType to match the claim coming from your token provider, but this email claim is the default identity claim for ADFS. It’s very uncommon to change the GroupClaimType. This codebase is written to only support two claims, users and groups. Extending the code to support other claims is beyond the scope of this article.

For the remaining fields, the developer should not need to modify any values unless undergoing an advanced customization.

conststring StringTypeClaim = Microsoft.IdentityModel.Claims.ClaimValueTypes.String;

publicoverridestring Name { get { return ClaimProviderDisplayName; } }

publicoverridebool SupportsEntityInformation { get { returnfalse; } }

publicoverridebool SupportsHierarchy { get { returntrue; } }

publicoverridebool SupportsResolve { get { returntrue; } }

publicoverridebool SupportsSearch { get { returntrue; } }

For the methodsFillClaimTypes,FillClaimValueTypesandFillEntityTypes, there are two important things to note. The first is that we actually add the StringTypeClaim twice, once for email value and once for group SID value. The second is that we use the value SPClaimEntityTypes.SecurityGroup for the group claim. Different people editor controls support different kinds of SPClaimEntityTypes. The two most common are User and SecurityGroup.

protectedoverridevoid FillClaimTypes(Liststring> claimTypes)

{

claimTypes.Add(IdentityClaimType);

claimTypes.Add(GroupClaimType);

}

protectedoverridevoid FillClaimValueTypes(Liststring> claimValueTypes)

{

// EmailAddress

claimValueTypes.Add(StringTypeClaim);

// Group

claimValueTypes.Add(StringTypeClaim);

}

protectedoverridevoid FillEntityTypes(Liststring> entityTypes)

{

// EmailAddress

entityTypes.Add(SPClaimEntityTypes.User);

// Group

entityTypes.Add(SPClaimEntityTypes.FormsRole);

}

We do not perform any claims augmentation, so the method FillClaimsForEntitydoes nothing

protectedoverridevoid FillClaimsForEntity(System.Uri context, SPClaim entity, ListSPClaim> claims)

{

}

The FillHierarchy method is called recursively to build the hierarchy on the left side of the details view. For our claim provider we only add users and groups as children to the root of the claim provider.

protectedoverridevoid FillHierarchy(System.Uri context, string[] entityTypes, string hierarchyNodeID, int numberOfLevels, SPProviderHierarchyTree hierarchy)

{

//inherited from MSDN code sample

if (!EntityTypesContain(entityTypes, SPClaimEntityTypes.FormsRole))

return;

switch (hierarchyNodeID)

{

//Add our users and groups nodes - we don't build a hierarchy but this is possible

casenull:

hierarchy.AddChild(newSPProviderHierarchyNode(CustomClaimProvider.ClaimProviderDisplayName,

usersClaim,

usersClaim,

true));

hierarchy.AddChild(newSPProviderHierarchyNode(CustomClaimProvider.ClaimProviderDisplayName,

groupsClaim, groupsClaim, true));

break;

default:

break;

}

}

Figure 9 – The Claim Hierarchy

Now we’ll examine the methods FillSearch and FillResolve. Both of these can really be combined into a single method FillSearchAndResolve, which will be explained later.

protectedoverridevoid FillSearch(System.Uri context, string[] entityTypes, string searchPattern, string hierarchyNodeID, int maxCount, SPProviderHierarchyTree searchTree)

{

//we actually tackle searching and resolving in one method...

FillSearchAndResolve(searchPattern, searchTree, null, hierarchyNodeID, maxCount);

}

protectedoverridevoid FillResolve(System.Uri context, string[] entityTypes, string resolveInput, ListPickerEntity> resolved)

{

//we actually tackle searching and resolving in one method...

FillSearchAndResolve(resolveInput, null, resolved, null, 30);

}

The FillSchema method is used to add information to the detail and list view of the people picker.

protectedoverridevoid FillSchema(SPProviderSchema schema)

{

//Lets show the users title in the details view, just for giggles

schema.AddSchemaElement(newSPSchemaElement(PeopleEditorEntityDataKeys.JobTitle,

"Title",

SPSchemaElementType.DetailViewOnly));

}

Figure 11 – Job Title Added to the Details View

There is a second FillResolve method from the base class that passes in a claim instead of a search string. This method runs whenever a user views a peopleeditor control with existing values inside it. Each entry is passed through this method to make sure all entries are still valid. If the claim type coming in for validation is a user claim, we search all of AD for a single entry with objectclass user and a value equal to whatever we choose for userLDAPUniqueIdentifier, in this case mail. If the incoming claim type is a group, we search all of AD for a single entry with objectclass group and a value equal to the group SID.

protectedoverridevoid FillResolve(System.Uri context, string[] entityTypes, SPClaim resolveInput, ListPickerEntity> resolved)

{

try

{

//Dunno exactly why we do this, but MSFT written codeplex peoplepicker does it too...

SPSecurity.RunWithElevatedPrivileges(delegate()

{

// null means all domains - we search all domains here because its fast and theres no hint on which domain to use

// when resolving

var directorySearcher = CreateDirectorySearcher(null);

SearchResult result = null;

switch (resolveInput.ClaimType)

{

//Open a people editor with an existing user

case IdentityClaimType:

directorySearcher.Filter = "(&(objectClass=user)(" + userLDAPUniqueIdentifier + "=" + resolveInput.Value + "))";

LoadDSProperties(directorySearcher);

//we don't worry if there are multiple results - we assume totally unique values for email across domains

result = directorySearcher.FindOne();

resolved.Add(CreatePickerEntityForUser(result));

break;

//Open a people editor with an existing group

case GroupClaimType:

var sid = newSecurityIdentifier(resolveInput.Value);

byte[] objSID = newbyte[sid.BinaryLength];

sid.GetBinaryForm(objSID, 0);

StringBuilder hexSID = newStringBuilder();

for (int i = 0; i < objSID.Length; i++)

{

hexSID.AppendFormat("\\{0:x2}", objSID[i]);

}

directorySearcher.Filter = "(&(objectClass=group)(objectsid=" + hexSID.ToString() + "))";

LoadDSProperties(directorySearcher);

result = directorySearcher.FindOne();

resolved.Add(CreatePickerEntityForGroup(result));

break;

default:

break;

}

});

}

catch (Exception ex)

{

SPDiagnosticsService.Local.WriteEvent(

0,

newSPDiagnosticsCategory("CustomClaimProvider", TraceSeverity.High, EventSeverity.Error), EventSeverity.Error,

"Unknown Error occurred inside FillSearchAndResolve - " + ex.ToString());

}

}

Now we come to the most important method in the class, FillSearchAndResolve. Let’s take this in pieces. First we look at the input string to see if someone entered a domain name such as “CHI\toddwilder”. In this case we restrict our AD search to the CHI domain. If we don’t see any domain specified, we search all domains.

// look for domain names in the searchstring - set FoundDomain and trim searchstring further

// if there is a domain found...

var domains = Forest.GetCurrentForest().Domains;

Domain foundDomain = null;

foreach (Domain domain in domains)

{

if (trimmedSearchPattern.StartsWith(domain.Name + "\\", StringComparison.CurrentCultureIgnoreCase))

{

trimmedSearchPattern = Regex.Replace(trimmedSearchPattern, "^" + domain.Name + "\\\\", "", RegexOptions.IgnoreCase);

foundDomain = domain;

break;

}

var domainName = domain.Name.Split('.')[0];

if (trimmedSearchPattern.StartsWith(domainName + "\\", StringComparison.CurrentCultureIgnoreCase))

{

trimmedSearchPattern = Regex.Replace(trimmedSearchPattern, "^" + domainName + "\\\\", "", RegexOptions.IgnoreCase);

foundDomain = domain;

break;

}

}

Now we create the directorysearcher object using the line

var directorySearcher = CreateDirectorySearcher(foundDomain);

Lets take a look at this method and its supporting objects. The purpose of this code is to create either a directorysearcher that searches all domains in the forest, or only searches a single domain.

//shamelessly lifted from

//

classDirectoryConstants

{

publicstaticstring RootDSE { get { return@"GC://rootDSE"; } }

publicstaticstring RootDomainNamingContext { get { return"rootDomainNamingContext"; } }

publicstaticstring GlobalCatalogProtocol { get { return@"GC://"; } }

}

privatestaticDirectorySearcher CreateDirectorySearcher(Domain domain)

{

var directorySearcher = newDirectorySearcher();

if (domain == null)

{

//Search all domains

using (DirectoryEntry directoryEntry = newDirectoryEntry(DirectoryConstants.RootDSE))

{

// Create a Global Catalog Directory Service Searcher

string strRootName = directoryEntry.Properties[DirectoryConstants.RootDomainNamingContext].Value.ToString();

using (DirectoryEntry usersBinding = newDirectoryEntry(DirectoryConstants.GlobalCatalogProtocol + strRootName))

{

directorySearcher.SearchRoot = usersBinding;

}

}

}

else

{

//just use one domain

directorySearcher.SearchRoot = domain.GetDirectoryEntry();

}

return directorySearcher;

}

Now let’s continue down the FillSearchAndResolve method. When a user is searching, they can specify whether to search users, groups or both. Let’s first examine the scenario when they do not specify either users or groups. This happens when they search inside the peopleeditor control, or they search in the popup window without selecting either the users or groups node.

if (string.IsNullOrEmpty(hierarchyId))

{

//If the user is not clicking on Users or Groups in the directory popup

directorySearcher.Filter = GetFilterStringForUserOrGroup(trimmedSearchPattern);

}

Let’s jump right to the GetFilterStringForUserOrGroup method for examination. Here we ask for AD results that are not disabled (userAccountControl:1.2.840.113556.1.4.803:=2), are either a person or group, and who’s displayname, givenname, surname, commonname or email start with the search string. The complex string replacement involving {1},{2} and {3} are in case someone entered a user name in the format “Todd Wilder". We then remove all newlines from the string and the filter string is ready to go.

privatestring GetFilterStringForUserOrGroup(string searchPattern)

{

var returnValue = @"

(

(!(userAccountControl:1.2.840.113556.1.4.803:=2))

(

|

(objectClass=User)

(objectClass=Group)

)

(

|

(sn={0}*)

(displayName={0}*)" + (searchPattern.Contains(" ") ? @"

(displayName={1}*)

(

&

(givenname={2})

(sn={3}*)

)" : "") + @"

(cn={0}*)

(mail={0}*)

(givenName={0}*)

)

)";

returnValue = Regex.Replace(returnValue, @"[ \r\n]", "", RegexOptions.None);

if (searchPattern.Contains(" "))

{

returnValue = ModifyReturnValueForPossibleFirstNameLastName(searchPattern, returnValue);

}

else

{

returnValue = string.Format(returnValue, searchPattern);

}

return returnValue;

}

GetFilterStringForUser and GetFilterStringForGroup are both variations of the filter shown above. These methods are called when we know the user is specifically searching for a user or group (they’ve selected either the user or group node in the popup window).

Let keep moving down the FillSearchAndResolve method. We Set the size limit, sort and load the AD properties to return in the search results

directorySearcher.SizeLimit = maxCount;

directorySearcher.Sort = newSortOption("displayname", SortDirection.Ascending);

LoadDSProperties(directorySearcher);

var results = directorySearcher.FindAll();

Lets take a quick look at LoadDSProperties. These properties are needed to create the PickerEntity objects that get rendered in the people picker.

privatestaticvoid LoadDSProperties(DirectorySearcher ds)

{

ds.PropertiesToLoad.Add("distinguishedName");

ds.PropertiesToLoad.Add("objectsid");

ds.PropertiesToLoad.Add("objectClass");

ds.PropertiesToLoad.Add("cn");

ds.PropertiesToLoad.Add("displayName");

ds.PropertiesToLoad.Add("description");

ds.PropertiesToLoad.Add("mail");

ds.PropertiesToLoad.Add("telephoneNumber");

ds.PropertiesToLoad.Add("title");

ds.PropertiesToLoad.Add("department");

ds.PropertiesToLoad.Add("chiportalid");

}

The two sorted lists deserve some explanation…

ListPickerEntity> sortedCorporateEntities = newListPickerEntity>();

ListPickerEntity> sortedNonCorporateEntities = newListPickerEntity>();

These lists allow us to render all sorted results from the “Corporate Domain” and then render all sorted results from all other domains. It’s important to note there is a bug in the people picker where this sorting is not always obeyed.

Let’s now take a look at how we render a user inside the people picker. We do this by creating a PickerEntity class from the SearchResult object.

if ((string)result.Properties["objectClass"][1] == "group")

{

pe = CreatePickerEntityForGroup(result);

}

Let’s dive into the CreatePickerEntityForGroup method. We create a PickerEntity object from the superclass method and get the group’s domain to show in the display name. We try to get the groups displayname but use the commonname as a “Plan B” display name. We then build the claim object based off the groups SID. The group SID is better than the group name, because the SID will (almost) never change and it does not have any exotic characters.

privatePickerEntity CreatePickerEntityForGroup(SearchResult result)

{

//Create and initialize new Picker Entity

PickerEntity entity = CreatePickerEntity();

var domainName = GetDomainName(result);

string groupId = (string)result.Properties["cn"][0];

string groupName = domainName + groupId;

if (result.Properties["displayName"] != null & result.Properties["displayName"].Count > 0)

{

groupName = domainName + (string)result.Properties["displayName"][0];

}

else

{

// is this possible?

}

var sid = newSecurityIdentifier((byte[])result.Properties["objectsid"][0], 0);

entity.Claim = newSPClaim(GroupClaimType,

sid.ToString(),

StringTypeClaim,

SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider,

SPTrustedIdentityTokenIssuerName));

entity.Description = ClaimProviderDisplayName + ":" + groupName;

entity.DisplayText = groupName;

entity.EntityType = SPClaimEntityTypes.FormsRole;

entity.IsResolved = true;

entity.EntityGroupName = "Security Group";

entity.EntityData[PeopleEditorEntityDataKeys.DisplayName] = groupName;

return entity;

}

Lets take a quick look at the GetDomainName method, becauase its where forestDN and forestName come into play. forestDN is necessary to determine domain name from distinguishedName, and forestName is the value we give PickerEntities coming from the root domain.

privateobject GetDomainName(SearchResult result)

{

var returnValue = "";

if (result.Properties["distinguishedName"] != null & result.Properties["distinguishedName"].Count > 0)

{

//try to pull the domain name into the display name of the result...

returnValue = Regex.Match((string)result.Properties["distinguishedName"][0], @"(?<=,DC\=).*?(?=," + forestDN + ")", RegexOptions.IgnoreCase).Value + "\\";

if (returnValue == "\\")

{

returnValue = forestName + "\\";

}

}

else

{

// is this possible?

}

return returnValue.ToUpper();

}

Lets also examine the CreatePickerEntityForUser method. This method is very similar to the CreatePickerEntityForGroup method, but at the end of the method, we build out some additional properties for users to display in the list view of the popup window.

We now choose whether to add the PickerEntity to the corporate sorted list (which is shown first) or the non-corporate sorted list (which is shown last). Here is where the corporateDomainName field comes into play, it determines if a PickerEntity is shown first or not.

if (pe.DisplayText.StartsWith(corporateDomainName + "\\",StringComparison.CurrentCultureIgnoreCase))

{

sortedCorporateEntities.Add(pe);

}

else

{

sortedNonCorporateEntities.Add(pe);

}

Here is a fun little feature. If there is both a Daniel Williams and Danielle Williams, Daniel Williams can type Williams, Daniel into the peopleeditor control and not have to choose between the two users. Instead, it allows for an exact match.

// If i'm not viewing the directory browser popup, but instead using the peopeeditor control...

if (searchTree == null)

{

// this is so Williams, Daniel resolves successfully instead of complaining of ambiguity between

// Williams, Daniel and Williams, Danielle

var exactMatch = pe.DisplayText.Substring(pe.DisplayText.IndexOf(@"\") + 1);

if (searchPattern.Equals(exactMatch, StringComparison.CurrentCultureIgnoreCase))

{

break;

}

}

Now we come to the end of the all-important FillSearchAndResolve method. We sort each list and then add the PickerEntities to either the search tree (if we are searching using the popup window) or the resolved list (if we are searching using the people editor).

//Sort each separately by display name

sortedCorporateEntities.Sort((x, y) => x.DisplayText.CompareTo(y.DisplayText));

sortedNonCorporateEntities.Sort((x, y) => x.DisplayText.CompareTo(y.DisplayText));

AddEntitiesToTreeOrList(sortedCorporateEntities, searchTree, resolvedList);

AddEntitiesToTreeOrList(sortedNonCorporateEntities, searchTree, resolvedList);

This concludes the explanation of the CustomPeoplePicker. Feel free to send any questions to