SyntaxHighlighter

Thursday, February 13, 2014

Creating Claims Provider for custom IP-STS


I was recently involved in troubleshooting Claims Provider written specifically for custom IP-STS provider. The STS authenticate external user to access public facing SharePoint site collection. The issue was occurring while adding external users to SharePoint group. After selecting User (through people picker control) and adding them to SP group, users were not able to login to the site.

But if I add external users through PowerShell script (see link below for script), users can login and see everything they are suppose to see. After some initial debugging I noticed, the user identity of user added with PowerShell and user added through people picker control were not matching.

//User identity from people picker control (incorrect identity)
c:0ǿ.c|myprovider|testuser2@rtest.com

//User identity using PowerShell script (correct identity)
i:05.t|my provider|testuser2@rtest.com

I googled and found this nice blog link which describes all parts of claims identity. People picker was generating “other claim type” (denoted by “c”) but SharePoint required “identity claim type” (denoted by “i”). And there was one more issue, the trusted issuer (‘myprovider’ and ‘my provider) were not matching either. “my provider” is name of IP-STS provider and “myprovider” is the name of Claims Provider.

With this information, I took another look at claim provider class and could see the issue with code. Here are all the changes which fixed the issue:

First, the claim provider was offering only 1 claim but STS was providing lot more than that. So I updated FillClaimTypes and FillClaimValueTypes.

protected override void FillClaimTypes(List<string> claimTypes)
{
 if (claimTypes == null)
  throw new ArgumentNullException("claimTypes");

 claimTypes.Add(Microsoft.IdentityModel.Claims.ClaimTypes.Role);
 claimTypes.Add(Microsoft.IdentityModel.Claims.ClaimTypes.Upn);
 claimTypes.Add(Microsoft.IdentityModel.Claims.ClaimTypes.Email);
 claimTypes.Add(Microsoft.IdentityModel.Claims.ClaimTypes.Surname);
 claimTypes.Add(Microsoft.IdentityModel.Claims.ClaimTypes.GivenName);
 claimTypes.Add(Microsoft.IdentityModel.Claims.ClaimTypes.System);
}

protected override void FillClaimValueTypes(List<string> claimValueTypes)
{
 if (claimValueTypes == null)
  throw new ArgumentNullException("claimValueTypes");

 claimValueTypes.Add(Microsoft.IdentityModel.Claims.ClaimValueTypes.String);
 claimValueTypes.Add(Microsoft.IdentityModel.Claims.ClaimValueTypes.String);
 claimValueTypes.Add(Microsoft.IdentityModel.Claims.ClaimValueTypes.String);
 claimValueTypes.Add(Microsoft.IdentityModel.Claims.ClaimValueTypes.String);
 claimValueTypes.Add(Microsoft.IdentityModel.Claims.ClaimValueTypes.String);
 claimValueTypes.Add(Microsoft.IdentityModel.Claims.ClaimValueTypes.String);
}

I am using only String types for all claims, but if you are using other value types, make sure the order of ClaimTypes and ClaimValueTypes matches.

Second, to fix trusted issuer mis-match, I replaced base CreateClaim function with this function.
private SPClaim MyCreateClaim(string myClaimType, 
    string myClaimValueType, 
    string myClaimValue)
{
 SPClaim myClaim = new SPClaim(myClaimType, myClaimValue, myClaimValueType,
     SPOriginalIssuers.Format(
        SPOriginalIssuerType.TrustedProvider, “xxx provider”));

    return myClaim;
}

The base CreateClaim function by default creates claim using Name property of SPClaimProvider class as trusted issuer.

Third, return the correct claim type i.e. identity claim. FillSearch method was creating and returning claim of type “FormsRole” which is good for adding additional claims (claim augmentation) but not identity claim. Here is the updated code:
protected override void FillSearch(Uri context, string[] entityTypes, 
    string searchPattern, string hierarchyNodeID, 
    int maxCount, SPProviderHierarchyTree searchTree)
{
    try
    {
        var ldapServer = ConfigurationManager.ConnectionStrings["LDAPConn"];
        var connectionString = ldapServer.ConnectionString;
        var de = new DirectoryEntry(connectionString, 
            LDAPCredentials.UserName, LDAPCredentials.Password,
            AuthenticationTypes.Secure);
        var search = new DirectorySearcher(de);

        search.Filter = 
            string.Format(
                "(&(objectClass=inetorgperson)(|(anr={0})(mail={0}*)))",
                 searchPattern);

        search.SearchScope = SearchScope.Subtree;
        search.PropertiesToLoad.Add("displayName"); 
        search.PropertiesToLoad.Add("mail"); 

        var searchResults = search.FindAll();
        if (searchResults.Count > 0)
        {
          var matches = new List<PickerEntity>();
          foreach (SearchResult profile in searchResults)
          {
            var ups = profile.GetDirectoryEntry();
            var pe = GetPickerEntity(ups.Properties["mail"].Value.ToString(),
               ups.Properties["displayName"].Value.ToString());
            matches.Add(pe);
          }
          searchTree.AddEntities(matches);
    }
    catch (Exception ex)
    {
        LoggingService.LogErrorInULS(ex, TraceSeverity.Unexpected);
    }
}

private PickerEntity GetPickerEntity(string claimValue, string preferredName)
{
    PickerEntity pe = CreatePickerEntity();
    //pe.Claim = CreateClaim(myClaimType, claimValue, myClaimValueType);     
    pe.Claim = MyCreateClaim(myClaimType, myClaimValueType, claimValue);
    pe.Description = string.Format( "{1} [{1}]", “My Provider”, claimValue);
    pe.DisplayText = preferredName ;
    pe.EntityData[PeopleEditorEntityDataKeys.DisplayName] = claimValue;
    pe.EntityType = SPClaimEntityTypes.User; // SPClaimEntityTypes.FormsRole;
    pe.IsResolved = true;
    return pe;
}

The change was simple, change EntityType to SPClaimEntityTypes.User from FormsRole and use custom function to create claim.

Build and deploy package to server.

Here are few links which helped me in resolving the issue:
Claims Walkthrough
Claims Encoding
Writing your own Trusted Identity provider for SP2010 (3) (this link has script to create user through PowerShell)

Hope this helps.

-Javed