SharePoint & Custom Claims when consuming service applications

Wictor Wilen has two great blogs out there that discuss the claim type encoding and a lot of the information I’ve gathered is from those blogs.

These were written for 2010 but a lot of it still applies. In fact, 99% of it still applies to 2013, and 2016. I can imagine the same will be for 2019.

http://www.wictorwilen.se/Post/How-Claims-encoding-works-in-SharePoint-2010.aspx

http://www.wictorwilen.se/introducing-the-sharepoint-2010-get-spclaimtypeencoding-and-new-spclaimtypeencoding-cmdlets

Consider the following configuration:

We have two farms configured. One farm is an Enterprise Services farm publishing service applications like Managed Metadata, User Profile Services, and Search. The other farm is the consuming farm or Content Farm. The consuming farm is authenticated via custom Claims from a third-party Identity provider like SiteMinder or Ping. When we are performing a search, we are getting no results returned when we are expecting to get many search results. When we are logged in with our windows account and not SAML we get results without issue.

  • Customer is logged in as: i:0g.t|siteminder|b0000001
  • Claim added to query: c:0?.t|siteminder|b0000001

For those of you who do not wish to read the whole blog the following two points are the cause of the issue

  • The underlying cause of this issue is due to custom claims and claim type encoding between the two farms.
  • In order to use custom claims between two or more farms their claim type encoding character must be the same between all the farms

But let’s walk through this a little so we know how to troubleshoot this

  • To begin troubleshooting this issue we must know what claim types our farm knows about, and what is the associated Encoding Character.
  • We simply run the following PowerShell command
  • This will give you the following output: (Example from SharePoint 2016)

    This will convert the encoding character to a more manageable value

  • Get-SPClaimTypeEncoding | ft @{Label=”Character”;Expression={[Convert]::ToInt32($_.EncodingCharacter)}}, ClaimType | ft -autosize

    New-SPClaimTypeEncoding

  • Using the New-SPClaimTypeEncoding cmdlet we can add our own claim types and use our own encoding character.
  • The cmdlet takes two arguments the -EncodingCharacter which is the encoded value and the -ClaimType.
    • New-SPClaimTypeEncoding -EncodingCharacter ([Convert]::ToChar(513)) -ClaimType “urn:another-claim-type” -Confirm:$false

  • If you get an Argument Exception, it can be for many different reasons.
    • Make sure that your encoding character is not currently in use.
    • Make sure you’re starting above 500 for your encoding character.
    • We cannot use upper case characters or white space characters.

    Note:

    • The use of the encoding character is the most important part here. We must use an encoding character that is not currently in use in the publishing farm or the consuming farm.
    • This is crucial because you can’t remove a custom claim once added so the encoding character cannot be reused.
    • This encoding character must be the same between farms for our custom claims.
      • If they are not, then when we go to match our custom claim type to our known claim types by the encoding character they will not match up and we will get security trimmed or access denied as the result.

    Clarification:

  • Publishing farm has custom claim “customID” with a claim type of http://somecustomclaim/claims/customID using encoding character 509.

  • Consuming farm has the same custom claim “customID” with a claim type of http://somecustomclaim/claims/customID but it’s using encoding character 505.

  • Between farms this “customID” claim is not the same claim even though they have the same claim name and claim type.
  • Since we map our claim type to our known claims list by the encoding character and not by claim type, or name, these would be treated as two different claims using the same claim name and claim type.

    So, the real question now is how do we fix this??

    Well since you can’t remove a claim once it’s been added

    And you can’t add a claim type if it already exists

    The only tangible way to fix this is to do one of two things

    • Rebuild the farm and add the claim type with the correct encoding character
    • Use a different OOTB claim or custom claim that has the same encoding character between farms

    Rebuilding the farm is straightforward, but what if that’s not an option and you can’t change the incoming claim type sent from the third-party identity provider on the consuming farm?

    • Well I suppose you can use SharePoint to transform the incoming claim type to a different claim type. Perhaps one that exists in the enterprise services farm.
      • I would suggest using an out of the box claim type and not a custom claim type.
    • This process would be done on the consuming farm and it would be done on the trusted identity provider.
    • The easiest way to accomplish this is when create our trusted identity provider when we are creating our claim mappings.
    • I won’t go into detail on how to add a trusted identity token issuer, or how to add a claim mapping to an existing trusted identity token issuer as that is outside the scope of this article. However, the claim mapping would look something like this where we take the incoming claim of “customID” and transform the claim to UPN. Since this is an out of the box claim and if we have permission to resources via the UPN claim type search results will start displaying proper results.

SharePoint and the infamous People Picker

So, I had a pretty interesting support case the other day. Cx stated that people picker wasn’t returning any results for users in a trusted domain. I was thinking ok I mean how hard could this be……. well so, my brain hurts now . So hopefully this will save someone else a huge headache.

Ok so let me paint the scenario for you, but first what is the issue?

Problem:

When you perform a people picker search for a user account that resides in a trusted domain no results are returned and SharePoint reports “User SPPrincipalInfo doesn’t seem to have a user principal name value” for our user we queried for

Scenario:

  • We have two domains with a two-way external trust configured. So, everyone trusts everyone here it’s a happy family.
    • Contoso.com
    • Fabrikam.com
  • SharePoint Servers, SharePoint Farm Service Accounts, and User accounts reside in the Contoso.com domain
  • For purposes of this article only user accounts reside in the Fabrikam.com domain
  • We are using a SAML provider that is using an out of the box AD claim provider. This is created by SharePoint when we create our trusted identity token issuer using the “-UseDefaultConfiguration” parameter like so:

    $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(“C:\temp\ADFSSigningCert.cer”)

    New-SPTrustedRootAuthority -Name “Token Signing Cert” -Certificate $cert

    $ap = New-SPTrustedIdentityTokenIssuer -Name “ADFS SAML Provider” -Description “SAML Authentication for SharePoint” -realm “urn:sharepoint:www.contoso.com” -ImportTrustCertificate $cert -SignInUrl “https://adfs.Contoso.com/adfs/ls-UseDefaultConfiguration -IdentifierClaimIs USER-PRINCIPAL-NAME;

    $ap = Get-SPTrustedIdentityTokenIssuer -Identity “ADFS SAML Provider”;

    $ap.ProviderRealms.Add(“https://www.contoso.com”, “urn:sharepoint:www.contoso.com”);

    $ap.ProviderSignOutUri = “https://adfs.contoso.com/adfs/ls“;

    $ap.ClaimProviderName = “ADFS SAML Provider”;

    $ap.UseWReplyParameter = $true;

    $ap.Update();

Note:

The -UseDefaultConfiguration parameter will create a claim provider for you. Since this is our code (Microsoft) we are able to sink our hooks into AD to perform the user lookup based on the information entered into the people picker. This allows us to return a friendly display name instead of the awful identifying claim, which is some cases could be an employee ID

  • We have not disabled the out of the box AD Claim provider, but we have hidden the provider like so:
    • Do not disabled this claim provider it will break a lot of things that required windows authentication

      $cpm = Get-SPClaimProviderManager

      $ad = get-spclaimprovider -identity “AD”

      $ad.IsVisible = $false

      $cpm.Update()

  • We then must configure the people picker to search both domains like so:
    • If your domain short name differs from the first part of your domains FQDN you will need to disable netbios DC resolve.
    • For example, your FQDN for your domain is “Contoso.com” the Pre-Windows 2000 value for your domain is “Corp” and you log in as “Corp\user”.
      • Since “Corp” does not match “Contoso” we must run this PS and set the proper value for the attribute “ShortDomainName”

        Add-PSSnapin Microsoft.SharePoint.PowerShell -ea silentlycontinue

        $farm = get-spfarm

        $farm.Properties

        $farm.Properties[“disable-netbios-dc-resolve”] = $true

        $farm.Properties

        $farm.Update()

      • Otherwise we only have the need to run this PowerShell

        $wa = Get-SPWebApplication https://www.contoso.com

        $adsearchobj1 = New-Object Microsoft.SharePoint.Administration.SPPeoplePickerSearchActiveDirectoryDomain

        $adsearchobj1.DomainName = “Contoso.com”

        $adsearchobj1.ShortDomainName = “Contoso” #Optional

        $adsearchobj1.IsForest = $true #$true for Forest, $false for Domain

        $wa.PeoplePickerSettings.SearchActiveDirectoryDomains.Add($adsearchobj1)

        $wa = Get-SPWebApplication https://www.contoso.com

        $adsearchobj2 = New-Object Microsoft.SharePoint.Administration.SPPeoplePickerSearchActiveDirectoryDomain

        $adsearchobj2.DomainName = “Fabrikam.com”

        $adsearchobj2.ShortDomainName = “Fabrikam” #Optional

        $adsearchobj2.IsForest = $true #$true for Forest, $false for Domain

        $wa.PeoplePickerSettings.SearchActiveDirectoryDomains.Add($adsearchobj2)

        $wa.Update()

Note:

  • If you’re not certain if your short name differs from your FQDN here is some PowerShell to help you.
    • This will only return the local domain information though.

    Get the FQDN domain name

    (gwmi win32_computersystem).Domain

Get the Short name for the domain

(net config workstation) -match ‘Workstation domain\s+\S+$’ -replace ‘.+?(\S+)$’,’$1′

  • The -UseDefaultConfiguration parameter will create a claim provider for you. Since this is our code we are able to sink our hooks into AD to perform the user lookup based on the information entered into the people picker. This allows us to return a friendly display name instead of the awful identifying claim, which is some cases could be an employee ID.

Search with people picker for the user account in the fabrikam.com domain

  • Attempt to grant permissions to the user in the Fabrikam domain “PeoplePickerTest” using the people picker from here: https://www.contoso.com/_layouts/15/user.aspx
  • Within People Picker search for “peoplepickertest”
  • Search result:
    • No results found is the result

What You’ll see in the ULS logs and Network Monitor trace

Looking at netmon trace we see the first call

24167        12:47:08 PM 11/17/2016        13.6861171        w3wp.exe        10.19.141.20        RHWDC18.contoso.com         LDAPSASLBuffer        LDAPSASLBuffer:BufferLength: 667, AuthMechanism: GSS-SPNEGO        {LDAP:686, TCP:682, IPv4:676}

Here is our query to the Contoso.com domain

Filter: (|(&(objectCategory=person)(|(anr=peoplepickertest*)(SamAccountName=peoplepickertest*)(userPrincipalName=peoplepickertest*)))(&(objectCategory=group)(BIT_AND: (groupType)&2147483648)(|(anr=peoplepickertest*)(SamAccountName=peoplepickertest*))))

Here are the attributes we are requesting

Attributes: ( objectSID )( mail )( mobile )( displayName )( title )( department )( proxyAddresses )( cn )( samAccountName )( groupType )( userAccountControl )( distinguishedName )( objectClass )( userPrincipalName )( msexchmasteraccountsid )

24168        12:47:08 PM 11/17/2016        13.6869698        w3wp.exe        RHWDC18.contoso.com         10.19.141.20        LDAPSASLBuffer        LDAPSASLBuffer:BufferLength: 1027, AuthMechanism: GSS-SPNEGO        {LDAP:686, TCP:682, IPv4:676}

Here is our first search result

SearchResultEntry: CN=peoplepickertest@farbrikam.com,OU=Fabrikam,OU=Contacts from SimpleSync,OU=User,OU=Contoso,DC=contoso,DC=com

Here are the attributes returned

+ PartialAttribute: objectClass=( top )( person )( organizationalPerson )( contact )

+ PartialAttribute: cn=( peoplepickertest@Fabrikam.com )

+ PartialAttribute: title=( Business Application Consultant )

+ PartialAttribute: distinguishedName=( CN=peoplepickertest@Fabrikam.com,OU=Fabrikam,OU=Contacts from SimpleSync,OU=RHDC,OU=Contoso,DC=contoso,DC=com )

+ PartialAttribute: displayName=( Jones, Bob (Fabrikam) )

+ PartialAttribute: department=( AD_Medmanagement )

+ PartialAttribute: proxyAddresses=( x500:/o=Contoso.com/ou=First Administrative Group/cn=Recipients/cn=peoplepickertest.Farbrikam.com1 )( x500:/o=Contoso/ou=First Administrative Group/cn=Recipients/cn=peoplepickertest@Fabrikam.com1 )( X500:/o=Contoso/ou=First Administrative Group/cn=R

+ PartialAttribute: mail=( peoplepickertest@Farbrikam.com )

+ PartialAttribute: msExchMasterAccountSid=( )

In ULS we’ll see these entries for this search

SearchFromGC name = Contoso.com. start

GetAccountNameFromSid “0x0105000000000005150000007B005C46C7C60121369482B9798A0500” start

“0x0105000000000005150000007B005C46C7C60121369482B9798A0500” returned. returnValue=True

GetAccountNameFromSid “0x0105000000000005150000001741550EDE09584B0363450079C20000” start

GetAccountNameFromSid “0x0105000000000005150000001741550EDE09584B0363450079C20000” returned. returnValue=True

SearchFromGC name = Contoso.com. returned. Result count = 2

0x0105000000000005150000001741550EDE09584B0363450079C20000 = Userid:S-1-5-21-240468247-1264060894-4547331-49785

We have returned an entry with the SID S-1-5-21-240468247-1264060894-4547331-49785

This SID translates to fabrikam\peoplepickertest, but it was returned from the Contoso.com domain

We can see in the network monitor trace that this is returned as a contact with the attribute value msExchMasterAccountSid and no UPN

We then call out to the Fabrikam.com domain

39482        12:47:20 PM 11/17/2016        25.4775491        w3wp.exe        10.19.141.20        dc2vm.fabrikcam.com        LDAPSASLBuffer        LDAPSASLBuffer:BufferLength: 418, AuthMechanism: GSS-SPNEGO        {LDAP:1013, TCP:1012, IPv4:1009}

Here we search fabrikcam.com

SearchRequest: BaseDN: DC=Fabrikam,DC=com, SearchScope: WholeSubtree, SearchAlias: neverDerefAliases

We filter on SID

Filter: (&(objectSID=))

And we request these attributes

Attributes: ( objectSID )( mail )( mobile )( displayName )( title )( department )( proxyAddresses )( cn )( samAccountName )( groupType )( userAccountControl )( distinguishedName )( objectClass )( userPrincipalName )( msexchmasteraccountsid )

Here is the search result

39519        12:47:20 PM 11/17/2016        25.4835293        w3wp.exe        dc2vm.fabrikam.com        10.19.141.20        LDAPSASLBuffer        LDAPSASLBuffer:BufferLength: 789, AuthMechanism: GSS-SPNEGO        {LDAP:1013, TCP:1012, IPv4:1009}

SearchResultEntry: CN=PeoplePickerTest,OU=Contract,DC=Fabrikam,DC=Com

We return these attribute values

Frame: Number = 39519, Captured Frame Length = 847, MediaType = ETHERNET

– Attributes: 12 Partial Attributes

+ SequenceHeader:

+ PartialAttribute: objectClass=( top )( person )( organizationalPerson )( user )

+ PartialAttribute: cn=( PeoplePickerTest )

+ PartialAttribute: title=( Business Application Consultant )

+ PartialAttribute: distinguishedName=( CN=PeoplePickerTest,OU=Contract,DC=Fabrikam,DC=Com )

+ PartialAttribute: displayName=( PeoplePickerTest)

+ PartialAttribute: department=( AD_Medmanagement )

+ PartialAttribute: proxyAddresses=( smtp:PeoplePickerTest@fabrikam.com )( SMTP:PeoplePickerTest@fabrikam.com)

+ PartialAttribute: userAccountControl=( 512 )

+ PartialAttribute: objectSid=( )

+ PartialAttribute: sAMAccountName=( PeoplePickerTest )

+ PartialAttribute: userPrincipalName=( PeoplePickerTest@fabrikam.com )

+ PartialAttribute: mail=( PeoplePickerTest@fabrikam.com )

We then see these entries in the ULS for this search

SearchFromGC name = Fabrikam.com. start

GetAccountNameFromSid “0x0105000000000005150000001741550EDE09584B0363450079C20000” start

GetAccountNameFromSid “0x0105000000000005150000001741550EDE09584B0363450079C20000” returned. returnValue=True

SearchFromGC name = Fabrikam.com. returned. Result count = 1

We then see this error

User SPPrincipalInfo doesn’t seem to have a user principal name value. User: ‘fabrikam\PeoplePickerTest’, ID: ‘-1’

Conclusion:

  • We bind to the first result received with the SID
  • The attributes passed in the first search result was for a contact which does not have an object SID but does have a msEXCHMasterSID.
  • This attribute is included in the first search results, so we bind to this master SID and translate to a user account, which is the account we are expecting.
  • However, this search result does not contain a UserPrincipalName attribute, so we do not display the user.

Workarounds (in no particular order):

  • If you change the search order of the domains in the people picker searchadforests property to search the trusted domain Fabrikam.com first, then search Contoso.com. The people picker will bind to the first result it receives which will be the correct user object.

    Origninal configuration

    stsadm -o setproperty -url https://www.contoso.com -pn peoplepicker-searchadforests -pv “forest:contoso.com;forest:fabrikam.com”

    Reversed search order

    stsadm -o setproperty -url https://www.contoso.com -pn peoplepicker-searchadforests -pv “forest:fabrikam.com;forest:contoso.com

  • If you search for the user using domain\username that will return the correct results regardless of the people picker search order
    • This does not query every domain in the list
    • This is a direct LDAP query to the domain specified
  • If you copy the account name and paste it into the people picker search this will also return the correct user
    • This mimics the check name function for the account name
    • When you type in a user account we will actually start performing LDAP queries to return best bet results
      • For example, if I want to search for the account UserA as I type that into the people picker search the results may start displaying all the accounts with User in them and ultimately filter down to the exact result when you finish typing in the user account name
  • If you type in the users account name and then do a CTRL + K (check name function) this is the same as pasting in the users account name
    • This also uses the check name function

More of a permanent solution:

Ultimately the issue is people picker is resolving contact objects.  Since contacts can’t be used to log in with we can filter these out from the people picker results with a custom filter.

The filter is a LDAP search filter: (|(objectCategory=group)(&(objectCategory=person)(!objectClass=contact))).

We can test this filter using the custom search feature in Active Directory Users and Computers.

In SharePoint, we’ll fix this issue by using the below PowerShell:

==============================PowerShell Script==============================

Add-PSSnapin Microsoft.SharePoint.PowerShell -ea silentlycontinue

$wa = Get-SPWebApplication <Put Web App Here>

$wa.PeoplePickerSettings.SearchActiveDirectoryDomains | out-file c:\temp\pp_settings_before.txt

$wa.PeoplePickerSettings.SearchActiveDirectoryDomains.Clear()

$wa.update()

$wa.PeoplePickerSettings.SearchActiveDirectoryDomains

$wa = Get-SPWebApplication <Put Web App Here>

$adsearchobj1 = New-Object Microsoft.SharePoint.Administration.SPPeoplePickerSearchActiveDirectoryDomain

$adsearchobj1.DomainName = “Contoso.com”

$adsearchobj1.ShortDomainName = “Contoso”

$adsearchobj1.IsForest = $false

$newdomain.CustomFilter = “(|(objectCategory=group)(&(objectCategory=person)(!objectClass=contact)))”

$wa.PeoplePickerSettings.SearchActiveDirectoryDomains.Add($adsearchobj1)

$wa = Get-SPWebApplication <Put Web App Here>

$adsearchobj2 = New-Object Microsoft.SharePoint.Administration.SPPeoplePickerSearchActiveDirectoryDomain

$adsearchobj2.DomainName = “Fabrikam.com”

$adsearchobj2.ShortDomainName = “fabrikam”

$adsearchobj2.IsForest = $true

$newdomain2.CustomFilter = “(|(objectCategory=group)(&(objectCategory=person)(!objectClass=contact)))”

$wa.PeoplePickerSettings.SearchActiveDirectoryDomains.Add($adsearchobj2)

$wa.Update()

==============================PowerShell Script==============================

Additional information about People Picker and the MSExchMasterAccountSid property
https://blogs.iis.net/deanc/troubleshooting-people-picker-with-netmon

To see what is currently set in your environment use the following PowerShell
$webService = new-object Microsoft.SharePoint.Administration.SPWebService
$webService.PeoplePickerSearchReplicatedMasterSIDPropertyName

Now this is ignored if you copy paste in a value that exists in the local domain. This will return the local domains user object instead of the referenced object that is in the MSExchMasterAccountSid property