Recently one of our customers needed a way to move incoming claims from SiteMinder/ADFS into the user properties of the person logging in. In our case, it was critical to have the e-mail claim be passed into the “Work E-mail” user property on a private SharePoint portal. While it is true that there exists built-in functionality in SharePoint 2010 that will map user properties from Active Directory to user properties and synchronize them, it will not work for the ADFS authentication claims. In our case the users we were concerned with were not the users on our customer’s active directory server, but on an external server. For this reason, any incoming claims, including the e-mail claim, were just simply ignored by SharePoint.
Microsoft advised us to develop a custom claims provider, and the task ultimately came down to me. This was entirely alien to me as I had never done anything like this before, but I managed to pull it together in about a week’s time. Given how much documentation I had to search through to achieve this seemingly simple goal, I would like to save someone out there the trouble by disclosing some of the techniques I used to achieve this.
Understanding Claims-Based Authentication
There’s a decent Wikipedia page about claim-based identity that can help you out more on this but essentially it distills down to a system similar to OAuth where you have an authentication provider, a server that you provide credentials to which does not have anything to do with your service aside from the fact that you’re using their user database. Your client authenticates through that server and the server will provide your web service with whatever claims are configured (claims in this case being properties about the authenticating user that you wish to know or validate against).
Intercepting the default SharePoint authentication behavior for claims
After authenticating, the user will be redirected, along with a SAML document that is passed along, to the SharePoint site’s /_trust/ URL which will verify the token and run any customized claims processing. This is our entry point for the custom claims provider.
There’s another Wikipedia article about SAML (Security Assertion Markup Language) which you may find useful. It shows you some of the basic structure and flow used.
Overview of writing a Custom Claims Provider
The process is ultimately quite simple, after you get it.
- Create an empty SharePoint project in Visual Studio
- Create a class that implements the abstract SPClaimProvider
- Implement a couple of methods and properties (the main ones you will need are FillClaimsForEntity, and the DisplayName property)
- Add a SharePoint “feature” into the project and set up its properties
- Compile and deploy the application either through visual studio itself or by running some PowerShell magic. In my case, I had to make use of both since VS2010 is not exactly the most stable piece of software.
That’s essentially it. Sounds simple enough? Let’s get started.
The Set-up
In Visual Studio, create a new project in your solution. It should be an “Empty Sharepoint Project”. Create a class that will implement SPClaimProvider. At this point I won’t bother explaining too much detail because you will basically need to create a skeleton of all the methods you are going to implement. Follow the examples shown in the “Additional References” section below. Here’s the most important things out of what you will need to know, read it carefully!
- The method that will be called for you when the user is authenticating is FillClaimsForEntity. This is your entry point to work with the authentication system and where you will put most of your logic.
- The method FillClaimsForEntity will only be called if you have the property SupportsEntityInformation set to true.
- In most situations when you use custom claims providers, you’re going to want to take some external claims and then add them to SharePoint claims. This is done by inserting them into the ‘claims’ list parameter that is passed along to you in FillClaimsForEntity. However, in my case I needed to update user properties with the data stored in the e-mail claim. If you are trying to accomplish something similar, you will not need that parameter.
- Make sure your feature is set as a farm feature and not a web feature
- Most of the other methods you will see in custom claim providers such as FillClaimTypes and FillClaimValueTypes you can just leave blank as for the functionality I described they are not needed. These two methods in specific that I just mentioned are required if you’re passing along the claims into SharePoint via the claims parameter.
- All the “Supports” properties like “SupportsSearch” should be false except for the one I already mentioned, SupportsEntityInformation which should be true.
The Code
For obvious reasons I am not going to disclose the exact code I used as it is confidential to our customers but I will provide helpful snippets and links to some of the places I got them from.
The bulk of your code is going to be in the FillClaimsForEntity method, so let’s start off by looking at the process for it. At a high level you’re going to want to extract the incoming claims from the SAML document, locate the user associated with those claims in SharePoint, verify if any updates need to occur, and then perform the updates on the user properties that need updating. For the first part of these tasks, there is a very, incredibly helpful post about it on TechNet. Essentially, it distills down to using System.ServiceModel.OperationContext.Current.RequestContext.RequestMessage.ToString to get the string representation of the SAML document with all the claims in it, then just navigating through the XML DOM using System.Xml. There’s a full code snippet in that link. Some customizations I made were changing the incomingClaims into a dictionary instead of a list since we were only concerned with one claim of each type (e.g. only one e-mail) and found using a dictionary less bulky for retrieving the claim afterwards.
For the next part, you will need to know the username of the authenticating user. For this, I found the following useful snippet:
string userLoginEncoded = SPUtility.FormatAccountName("i", entity.Value);
string userLogin = SPClaimProviderManager.Local.DecodeClaim(userLoginEncoded).Value;
You may not need both for your purposes. In any case, userLoginEncoded represents the “i:…” format of username where as userLogin represents simply the username portion of it. To locate and update the user in Sharepoint we use the following code. Note that users don’t always have enough permissions to update their own properties, so you may need to run the code with elevated privileges like I have.
using (SPSite _site = new SPSite(*YOUR_SITE_URL_HERE*))
{
using (SPWeb _web = _site.OpenWeb())
{
SPSecurity.RunWithElevatedPrivileges(delegate()
{
SPUser _myUser = _web.AllUsers[userLoginEncoded];
if (String.Compare(_myUser.Email.ToLower(), incomingClaims[IncomingEmailClaimType].claimValue.ToLower()) != 0)
{
_myUser.Email = incomingClaims[IncomingEmailClaimType].claimValue;
_myUser.Update();
}
});
}
}
The rest of my methods are empty. I implement the Name property as per the name I wish to give my claim provider, and set SupportsEntityInformation to true, and the other Supports properties to false. Note that for the *YOUR_SITE_URL_HERE* you will need to provide the actual site URL and not just the host. Even though your SharePoint site may be rooted at http://whatever/, you will need to input http://whatever/sites/mysite/ instead here. To confirm that you have the right URL, you can use PowerShell and run Get-SPUser on the site you are implementing this for and see if the relevant users are there or not (e.g. if I’m trying to login as user1 and user1 does not exist in the list of users when I do Get-SPUser on http://foo/, then my code to find the user profile will also not work).
Make sure you add your Feature’s EventReceiver and update the properties in it (the instructions for this are outlined in MSDN).
Useful Tips for Debugging
- Use Debug->Attach To Process and attach to all “w3wp.exe” processes (IIS app pool); this way you can make use of System.Diagnostics.Debug.WriteLine() in your code and see the output in Visual Studio.
- When you run the “Deploy” command on your project, one of the steps VS will take is to recycle the application pool for your SharePoint website. However, claims and authentication are not run in the application pool of the SharePoint site but in a separate app pool called SecurityTokenApplication. In order to make sure your code is running up-to-date in SharePoint you will need to recycle that app pool as well manually.
- When “Deploy”ing your application from VS you may encounter errors that the application pool could not be restarted because it couldn’t find the site. This may happen during the “Recycle Application Pool” step of deployment (in which case, try running Remove-SPClaimProvider in powershell before deploying) or during “activating features” in which case you don’t even need that step because farm features are automatically activated (so you can just change your deploy mode to “without activation” in that case).
- You may want to increase the timeout period in IIS because while debugging you will probably hit the timeout often.
Useful tips for Deploying
In the SharePoint powershell, you can use Get-SPWebApplication to get the list of SharePoint applications. Locate the display name of the one you are developing for, and note the name. Then, execute Get-SPClaimProvider and find the claim provider you just wrote in the list (note that this will require doing Add-SPSolution (on the *.wsp project that will be in your project folder under /bin/*/*.wsp) and Add-SPClaimProvider beforehand). Given these two, you will then need to do something like the following to add your claim provider to the site you are working with:
$WebApplication = Get-SPWebApplication *YOUR_APPLICATION_NAME*
$IisSettings = $WebApplication.GetIisSettingsWithFallback("Internet")
$providers = $IisSettings.ClaimsProviders
$providers += *REFERENCE_TO_YOUR_PROVIDER_FROM_Get-SPClaimProvider*
Set-SPWebApplication -Identity $WebApplication -Zone "Default" -AdditionalClaimProvider $providers
Set-SPWebApplication -Identity $WebApplication -Zone "Internet" -AdditionalClaimProvider $providers
Additional References