The Azure Custom Claim Provider for SharePoint Project Part 3
In Part 1 of this series, I briefly outlined the goals for this project, which at a high level is to use Windows Azure table storage as a data store for a SharePoint custom claims provider. The claims provider is going to use the CASI Kit to retrieve the data it needs from Windows Azure in order to provide people picker (i.e. address book) and type in control name resolution functionality.
In Part 2, I walked through all of the components that run in the cloud – the data classes that are used to work with Azure table storage and queues, a worker role to read items out of queues and populate table storage, and a WCF front end that lets a client application create new items in the queue as well as do all the standard SharePoint people picker stuff – provide a list of supported claim types, search for claim values and resolve claims.
In this, the final part in this series, we’ll walk through the different components used on the SharePoint side. It includes a custom component built using the CASI Kit to add items to the queue as well as to make our calls to Azure table storage. It also includes our custom claims provider, which will use the CASI Kit component to connect SharePoint with those Azure functions.
To begin with let’s take a quick look at the custom CASI Kit component. I’m not going to spend a whole lot of time here because the CASI Kit is covered extensively on this blog. This particular component is described in Part 3 of the CASI Kit series. Briefly though, what I’ve done is created a new Windows class library project. I’ve added references to the CASI Kit base class assembly and the other required .NET assemblies (that I describe in part 3). I’ve added a Service Reference in my project to the WCF endpoint I created in Part 2 of this project. Finally, I added a new class to the project and have it inherit the CASI Kit base class, and I’ve added the code to override the ExecuteRequest method. As you have hopefully seen in the CASI Kit series, here’s what my code looks like to override ExecuteRequest:
public class DataSource : AzureConnect.WcfConfig
{
public override bool ExecuteRequest()
{
try
{
//create the proxy instance with bindings and endpoint the base class
//configuration control has created for this
AzureClaims.AzureClaimsClient cust =
new AzureClaims.AzureClaimsClient(this.FedBinding,
this.WcfEndpointAddress);
//configure the channel so we can call it with
//FederatedClientCredentials.
SPChannelFactoryOperations.ConfigureCredentials<AzureClaims.IAzureClaims>
(cust.ChannelFactory,
Microsoft.SharePoint.SPServiceAuthenticationMode.Claims);
//create a channel to the WCF endpoint using the
//token and claims of the current user
AzureClaims.IAzureClaims claimsWCF =
SPChannelFactoryOperations.CreateChannelActingAsLoggedOnUser
<AzureClaims.IAzureClaims>(cust.ChannelFactory,
this.WcfEndpointAddress,
new Uri(this.WcfEndpointAddress.Uri.AbsoluteUri));
//set the client property for the base class
this.WcfClientProxy = claimsWCF;
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
//now that the configuration is complete, call the method
return base.ExecuteRequest();
}
}
“AzureClaims” is the name of the Service Reference I created, and it uses the IAzureClaims interface that I defined in my WCF project in Azure. As explained previously in the CASI Kit series, this is basically boilerplate code, I’ve just plugged in the name of my interface and class that is exposed in the WCF application. The other thing I’ve done, as is also explained in the CASI Kit series, is to create an ASPX page called AzureClaimProvider.aspx. I just copied and pasted in the code I describe in Part 3 of the CASI Kit series and substituted the name of my class and the endpoint it can be reached at. The control tag in the ASPX page for my custom CASI Kit component looks like this:
<AzWcf:DataSource runat="server" id="wcf" WcfUrl="https://spsazure.vbtoys.com/ AzureClaims.svc" OutputType="Page" MethodName="GetClaimTypes" AccessDeniedMessage="" />
The main things to note here are that I created a CNAME record for “spsazure.vbtoys.com” that points to my Azure application at cloudapp.net (this is also described in Part 3 of the CASI Kit). I’ve set the default MethodName that the page is going to invoke to be GetClaimTypes, which is a method that takes no parameters and returns a list of claim types that my Azure claims provider supports. This makes it a good test to validate the connectivity between my Azure application and SharePoint. I can simply go to https://anySharePointsite/_layouts/AzureClaimProvider.aspx and if everything is configured correctly I will see some data in the page. Once I’ve deployed my project by adding the assembly to the Global Assembly Cache and deploying the page to SharePoint’s _layouts directory that’s exactly what I did – I hit the page in one of my sites and verified that it returned data, so I knew my connection between SharePoint and Azure was working.
Now that I have the plumbing in place, I finally get to the “fun” part of the project, which is to do two things:
- 1. Create “some component” that will send information about new users to my Azure queue
- 2. Create a custom claims provider that will use my custom CASI Kit component to provide claim types, name resolution and search for claims.
This is actually a good point to stop and step back a little. In this particular case I just wanted to roll something out as quickly as possible. So, what I did is I created a new web application and I enabled anonymous access. As I’m sure you all know, just enabling it at the web app level does NOT enable it at the site collection level. So for this scenario, I also enabled at the root site collection only; I granted access to everything in the site. All other site collections, which would contain any information for members only, does NOT have anonymous enabled so users have to be granted rights to join.
The next thing to think about is how to manage the identities that are going to use the site. Obviously, that’s not something I want to do. I could have come up with a number of different methods to sync accounts into Azure or something goofy like that, but as I explained in Part 1 of this series, there’s a whole bunch of providers that do that already so I’m going to let them keep doing what they do. What I mean by that is that I took advantage of another Microsoft cloud service called ACS, or Access Control Service. In short ACS acts like an identity provider to SharePoint. So I just created a trust between my SharePoint farm and the instance of ACS that I created for this POC. In ACS I added SharePoint as a relying party, so ACS knows where to send users once they’ve authenticated. Inside of ACS, I also configured it to let users sign in using their Gmail, Yahoo, or Facebook accounts. Once they’ve signed in ACS gets a single claim back that I’ll use – email address – and sends it back to SharePoint.
Okay, so that’s all of the background on the plumbing – Azure is providing table storage and queues to work with the data, ACS is providing authentication services, and CASI Kit is providing the plumbing to the data.
So with all that plumbing described, how are we going to use it? Well I still wanted the process to become a member pretty painless, so what I did is I wrote a web part to add users to my Azure queue. What it does is it checks to see if the request is authenticated (i.e. the user has clicked the Sign In link that you get in an anonymous site, signed into one of the providers I mentioned above, and ACS has sent me back their claim information). If the request is not authenticated the web part doesn’t do anything. However if the request is authenticated, it renders a button that when clicked, will take the user’s email claim and add it to the Azure queue. This is the part about which I said we should step back a moment and think about it. For a POC that’s all fine and good, it works. However you can think about other ways in which you could process this request. For example, maybe you write the information to a SharePoint list. You could write a custom timer job (that the CASI Kit works with very nicely) and periodically process new requests out of that list. You could use the SPWorkItem to queue the requests up to process later. You could store it in a list and add a custom workflow that maybe goes through some approval process, and once the request has been approved uses a custom workflow action to invoke the CASI Kit to push the details up to the Azure queue. In short – there’s a LOT of power, flexibility and customization possible here – it’s all up to your imagination. At some point in fact I may write another version of this that writes it to a custom list, processes it asynchronously at some point, adds the data to the Azure queue and then automatically adds the account to the Visitors group in one of the sub sites, so the user would be signed up and ready to go right away. But that’s for another post if I do that.
So, all that being said – as I described above I’m just letting the user click a button if they’ve signed in and then I’ll use my custom CASI Kit component to call out the WCF endpoint and add the information to the Azure queue. Here’s the code for the web part – pretty simple, courtesy of the CASI Kit:
public class AddToAzureWP : WebPart
{
//button whose click event we need to track so that we can
//add the user to Azure
Button addBtn = null;
Label statusLbl = null;
protected override void CreateChildControls()
{
if (this.Page.Request.IsAuthenticated)
{
addBtn = new Button();
addBtn.Text = "Request Membership";
addBtn.Click += new EventHandler(addBtn_Click);
this.Controls.Add(addBtn);
statusLbl = new Label();
this.Controls.Add(statusLbl);
}
}
void addBtn_Click(object sender, EventArgs e)
{
try
{
//look for the claims identity
IClaimsPrincipal cp = Page.User as IClaimsPrincipal;
if (cp != null)
{
//get the claims identity so we can enum claims
IClaimsIdentity ci = (IClaimsIdentity)cp.Identity;
//look for the email claim
//see if there are claims present before running through this
if (ci.Claims.Count > 0)
{
//look for the email address claim
var eClaim = from Claim c in ci.Claims
where c.ClaimType == "https://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
select c;
Claim ret = eClaim.FirstOrDefault<Claim>();
if (ret != null)
{
//create the string we're going to send to the Azure queue: claim, value, and display name
//note that I'm using "#" as delimiters because there is only one parameter, and CASI Kit
//uses ; as a delimiter so a different value is needed. If ; were used CASI would try and
//make it three parameters, when in reality it's only one
string qValue = ret.ClaimType + "#" + ret.Value + "#" + "Email";
//create the connection to Azure and upload
//create an instance of the control
AzureClaimProvider.DataSource cfgCtrl = new AzureClaimProvider.DataSource();
//set the properties to retrieve data; must configure cache properties since we're using it programmatically
//cache is not actually used in this case though
cfgCtrl.WcfUrl = AzureCCP.SVC_URI;
cfgCtrl.OutputType = AzureConnect.WcfConfig.DataOutputType.None;
cfgCtrl.MethodName = "AddClaimsToQueue";
cfgCtrl.MethodParams = qValue;
cfgCtrl.ServerCacheTime = 10;
cfgCtrl.ServerCacheName = ret.Value;
cfgCtrl.SharePointClaimsSiteUrl = this.Page.Request.Url.ToString();
//execute the method
bool success = cfgCtrl.ExecuteRequest();
if (success)
{
//if it worked tell the user
statusLbl.Text = "<p>Your information was successfully added. You can now contact any of " +
"the other Partner Members or our Support staff to get access rights to Partner " +
"content. Please note that it takes up to 15 minutes for your request to be " +
"processed.</p>";
}
else
{
statusLbl.Text = "<p>There was a problem adding your info to Azure; please try again later or " +
"contact Support if the problem persists.</p>";
}
}
}
}
}
catch (Exception ex)
{
statusLbl.Text = "There was a problem adding your info to Azure; please try again later or " +
"contact Support if the problem persists.";
Debug.WriteLine(ex.Message);
}
}
}
So a brief rundown of the code looks like this: I first make sure the request is authenticated; if it is I added the button to the page and I add an event handler for the click event of the button. In the button’s click event handler I get an IClaimsPrincipal reference to the current user, and then look at the user’s claims collection. I run a LINQ query against the claims collection to look for the email claim, which is the identity claim for my SPTrustedIdentityTokenIssuer. If I find the email claim, I create a concatenated string with the claim type, claim value and friendly name for the claim. Again, this isn’t strictly required in this scenario, but since I wanted this to be usable in a more generic scenario I coded it up this way. That concatenated string is the value for the method I have on the WCF that adds data to the Azure queue. I then create an instance of my custom CASI Kit component and configure it to call the WCF method that adds data to the queue, then I call the ExecuteRequest method to actually fire off the data.
If I get a response indicating I was successful adding data to the queue then I let the user know; otherwise I let him know there was a problem and hey may need to check again later. In a real scenario of course I would have even more error logging so I could track down exactly what happened and why. Even with this as is though, the CASI Kit will write any error information to the ULS logs in a SPMonitoredScope, so everything it does for the request will have a unique correlation ID with which we can view all activity associated with the request. So I’m actually in a pretty good state right now.
Okay – we’ve walked through all the plumbing pieces, and I’ve shown how data gets added to the Azure queue and from there pulled out by a worker process and added into table storage. That’s really the ultimate goal because now we can walk through the custom claims provider. It’s going to use the CASI Kit to call out and query the Azure table storage I’m using. Let’s look at the most interesting aspects of the custom claims provider.
First let’s look at a couple of class level attributes:
//the WCF endpoint that we'll use to connect for address book functions
//test url: https://az1.vbtoys.com/AzureClaimsWCF/AzureClaims.svc
//production url: https://spsazure.vbtoys.com/AzureClaims.svc
public static string SVC_URI = "https://spsazure.vbtoys.com/AzureClaims.svc";
//the identity claimtype value
private const string IDENTITY_CLAIM =
"https://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress";
//the collection of claim type we support; it won't change over the course of
//the STS (w3wp.exe) life time so we'll cache it once we get it
AzureClaimProvider.AzureClaims.ClaimTypeCollection AzureClaimTypes =
new AzureClaimProvider.AzureClaims.ClaimTypeCollection();
First, I use a constant to refer to the WCF endpoint to which the CASI Kit should connect. You’ll note that I have both my test endpoint and my production endpoint. When you’re using the CASI Kit programmatically, like we will in our custom claims provider, you always have to tell it where that WCF endpoint is that it should talk to.
Next, as I’ve described previously I’m using the email claim as my identity claim. Since I will refer to it a number of times throughout my provider, I’ve just plugged into a constant at the class level.
Finally, I have a collection of AzureClaimTypes. I explained in Part 2 of this series why I’m using a collection, and I’m just storing it here at the class level so that I don’t have to go and re-fetch that information each time my FillHierarchy method is invoked. Calls out to Azure aren’t cheap, so I minimize them where I can.
Here’s the next chunk of code:
internal static string ProviderDisplayName
{
get
{
return "AzureCustomClaimsProvider";
}
}
internal static string ProviderInternalName
{
get
{
return "AzureCustomClaimsProvider";
}
}
//*******************************************************************
//USE THIS PROPERTY NOW WHEN CREATING THE CLAIM FOR THE PICKERENTITY
internal static string SPTrustedIdentityTokenIssuerName
{
get
{
return "SPS ACS";
}
}
public override string Name
{
get
{
return ProviderInternalName;
}
}
The reason I wanted to point this code out is because since my provider is issuing identity claims, it MUST be the default provider for the SPTrustedIdentityTokenIssuer. Explaining how to do that is outside the scope of this post, but I’ve covered it elsewhere in my blog. The main thing to remember about doing that is that you must have a strong relationship between the name you use for your provider and the name used for the SPTrustedIdentityTokenIssuer. The value I used for the ProviderInternalName is the name that I must plug into the ClaimProviderName property for the SPTrustedIdentityTokenIssuer. Also, I need to use the name of the SPTrustedIdentityTokenIssuer when I’m creating identity claims for users. So I’ve created an SPTrustedIdentityTokenIssuer called “SPS ACS” and I’ve added that to my SPTrustedIdentityTokenIssuerName property. That’s why I have these values coded in here.
Since I’m not doing any claims augmentation in this provider, I have not written any code to override FillClaimTypes, FillClaimValueTypes or FillEntityTypes. The next chunk of code I have is FillHierarchy, which is where I tell SharePoint what claim types I support. Here’s the code for that:
try
{
if (
(AzureClaimTypes.ClaimTypes == null) ||
(AzureClaimTypes.ClaimTypes.Count() == 0)
)
{
//create an instance of the control
AzureClaimProvider.DataSource cfgCtrl = new AzureClaimProvider.DataSource();
//set the properties to retrieve data; must configure cache properties since we're using it programmatically
//cache is not actually used in this case though
cfgCtrl.WcfUrl = SVC_URI;
cfgCtrl.OutputType = AzureConnect.WcfConfig.DataOutputType.None;
cfgCtrl.MethodName = "GetClaimTypes";
cfgCtrl.ServerCacheTime = 10;
cfgCtrl.ServerCacheName = "GetClaimTypes";
cfgCtrl.SharePointClaimsSiteUrl = context.AbsoluteUri;
//execute the method
bool success = cfgCtrl.ExecuteRequest();
if (success)
{
//if it worked, get the list of claim types out
AzureClaimTypes =
(AzureClaimProvider.AzureClaims.ClaimTypeCollection)cfgCtrl.QueryResultsObject;
}
}
//make sure picker is asking for the type of entity we return; site collection admin won't for example
if (!EntityTypesContain(entityTypes, SPClaimEntityTypes.User))
return;
//at this point we have whatever claim types we're going to have, so add them to the hierarchy
//check to see if the hierarchyNodeID is null; it will be when the control
//is first loaded but if a user clicks on one of the nodes it will return
//the key of the node that was clicked on. This lets you build out a
//hierarchy as a user clicks on something, rather than all at once
if (
(string.IsNullOrEmpty(hierarchyNodeID)) &&
(AzureClaimTypes.ClaimTypes.Count() > 0)
)
{
//enumerate through each claim type
foreach (AzureClaimProvider.AzureClaims.ClaimType clm in AzureClaimTypes.ClaimTypes)
{
//when it first loads add all our nodes
hierarchy.AddChild(new
Microsoft.SharePoint.WebControls.SPProviderHierarchyNode(
ProviderInternalName, clm.FriendlyName, clm.ClaimTypeName, true));
}
}
}
catch (Exception ex)
{
Debug.WriteLine("Error filling hierarchy: " + ex.Message);
}
So here I’m looking to see if I’ve grabbed the list of claim types I support already. If I haven’t then I create an instance of my CASI Kit custom control and make a call out to my WCF to retrieve the claim types; I do this by calling the GetClaimTypes method on my WCF class. If I get data back then I plug it into the class-level variable I described earlier called AzureClaimTypes, and then I add it to the hierarchy of claim types I support.
The next methods we’ll look at is the FillResolve methods. The FillResolve methods have two different signatures because they do two different things. In one scenario we have a specific claim with value and type and SharePoint just wants to verify that is valid. In the second case a user has just typed some value into the SharePoint type in control and so it’s effectively the same thing as doing a search for claims. Because of that, I’ll look at them separately.
In the case where I have a specific claim and SharePoint wants to verify the values, I call a custom method I wrote called GetResolveResults. In that method I pass in the Uri where the request is being made as well as the claim type and claim value SharePoint is seeking to validate. The GetResolveResults then looks like this:
//Note that claimType is being passed in here for future extensibility; in the
//current case though, we're only using identity claims
private AzureClaimProvider.AzureClaims.UniqueClaimValue GetResolveResults(string siteUrl,
string searchPattern, string claimType)
{
AzureClaimProvider.AzureClaims.UniqueClaimValue result = null;
try
{
//create an instance of the control
AzureClaimProvider.DataSource cfgCtrl = new AzureClaimProvider.DataSource();
//set the properties to retrieve data; must configure cache properties since we're using it programmatically
//cache is not actually used in this case though
cfgCtrl.WcfUrl = SVC_URI;
cfgCtrl.OutputType = AzureConnect.WcfConfig.DataOutputType.None;
cfgCtrl.MethodName = "ResolveClaim";
cfgCtrl.ServerCacheTime = 10;
cfgCtrl.ServerCacheName = claimType + ";" + searchPattern;
cfgCtrl.MethodParams = IDENTITY_CLAIM + ";" + searchPattern;
cfgCtrl.SharePointClaimsSiteUrl = siteUrl;
//execute the method
bool success = cfgCtrl.ExecuteRequest();
//if the query encountered no errors then capture the result
if (success)
result = (AzureClaimProvider.AzureClaims.UniqueClaimValue)cfgCtrl.QueryResultsObject;
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
return result;
}
So here I’m creating an instance of the custom CASI Kit control then calling the ResolveClaim method on my WCF. That method takes two parameters, so I pass that in as semi-colon delimited values (because that’s how CASI Kit distinguishes between different param values). I then just execute the request and if it finds a match it will return a single UniqueClaimValue; otherwise the return value will be null. Back in my FillResolve method this is what my code looks like:
protected override void FillResolve(Uri context, string[] entityTypes, SPClaim resolveInput, List<PickerEntity> resolved)
{
//make sure picker is asking for the type of entity we return; site collection admin won't for example
if (!EntityTypesContain(entityTypes, SPClaimEntityTypes.User))
return;
try
{
//look for matching claims
AzureClaimProvider.AzureClaims.UniqueClaimValue result =
GetResolveResults(context.AbsoluteUri, resolveInput.Value,
resolveInput.ClaimType);
//if we found a match then add it to the resolved list
if (result != null)
{
PickerEntity pe = GetPickerEntity(result.ClaimValue, result.ClaimType,
SPClaimEntityTypes.User, result.DisplayName);
resolved.Add(pe);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
So I’m checking first to make sure that the request is for a User claim, since that the only type of claim my provider is returning. If the request is not for a User claim then I drop out. Next I call my method to resolve the claim and if I get back a non-null result, I process it. To process it I call another custom method I wrote called GetPickerEntity. Here I pass in the claim type and value to create an identity claim, and then I can add that PickerEntity that it returns to the List of PickerEntity instances passed into my method. I’m not going to go into the GetPickerEntity method because this post is already incredibly long and I’ve covered how to do so in other posts on my blog.
Now let’s talk about the other FillResolve method. As I explained earlier, it basically acts just like a search so I’m going to combine the FillResolve and FillSearch methods mostly together here. Both of these methods are going to call a custom method I wrote called SearchClaims, that looks like this:
private AzureClaimProvider.AzureClaims.UniqueClaimValueCollection SearchClaims(string claimType, string searchPattern,
string siteUrl)
{
AzureClaimProvider.AzureClaims.UniqueClaimValueCollection results =
new AzureClaimProvider.AzureClaims.UniqueClaimValueCollection();
try
{
//create an instance of the control
AzureClaimProvider.DataSource cfgCtrl = new AzureClaimProvider.DataSource();
//set the properties to retrieve data; must configure cache properties since we're using it programmatically
//cache is not actually used in this case though
cfgCtrl.WcfUrl = SVC_URI;
cfgCtrl.OutputType = AzureConnect.WcfConfig.DataOutputType.None;
cfgCtrl.MethodName = "SearchClaims";
cfgCtrl.ServerCacheTime = 10;
cfgCtrl.ServerCacheName = claimType + ";" + searchPattern;
cfgCtrl.MethodParams = claimType + ";" + searchPattern + ";200";
cfgCtrl.SharePointClaimsSiteUrl = siteUrl;
//execute the method
bool success = cfgCtrl.ExecuteRequest();
if (success)
{
//if it worked, get the array of results
results =
(AzureClaimProvider.AzureClaims.UniqueClaimValueCollection)cfgCtrl.QueryResultsObject;
}
}
catch (Exception ex)
{
Debug.WriteLine("Error searching claims: " + ex.Message);
}
return results;
}
In this method, as you’ve seen elsewhere in this post, I’m just creating an instance of my custom CASI Kit control. I’m calling the SearchClaims method on my WCF and I’m passing in the claim type I want to search in, the claim value I want to find in that claim type, and the maximum number of records to return. You may recall from Part 2 of this series that SearchClaims just does a BeginsWith on the search pattern that’s passed in, so with lots of users there could easily be over 200 results. However 200 is the maximum number of matches that the people picker will show, so that’s all I ask for. If you really think that users are going to scroll through more than 200 results looking for a result I’m here to tell you that ain’t likely.
So now we have our colletion of UniqueClaimValues back, let’s look at how we use it our two override methods in the custom claims provider. First, here’s what the FillResolve method looks like:
protected override void FillResolve(Uri context, string[] entityTypes, string resolveInput, List<PickerEntity> resolved)
{
//this version of resolve is just like a search, so we'll treat it like that
//make sure picker is asking for the type of entity we return; site collection admin won't for example
if (!EntityTypesContain(entityTypes, SPClaimEntityTypes.User))
return;
try
{
//do the search for matches
AzureClaimProvider.AzureClaims.UniqueClaimValueCollection results =
SearchClaims(IDENTITY_CLAIM, resolveInput, context.AbsoluteUri);
//go through each match and add a picker entity for it
foreach (AzureClaimProvider.AzureClaims.UniqueClaimValue cv in results.UniqueClaimValues)
{
PickerEntity pe = GetPickerEntity(cv.ClaimValue, cv.ClaimType, SPClaimEntityTypes.User, cv.DisplayName);
resolved.Add(pe);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
It just calls the SearchClaims method, and for each result it gets back (if any), it creates a new PickerEntity and adds it to the List of them passed into the override. All of them will show up in then in the type in control in SharePoint. The FillSearch method uses it like this:
protected override void FillSearch(Uri context, string[] entityTypes, string searchPattern, string hierarchyNodeID, int maxCount, SPProviderHierarchyTree searchTree)
{
//make sure picker is asking for the type of entity we return; site collection admin won't for example
if (!EntityTypesContain(entityTypes, SPClaimEntityTypes.User))
return;
try
{
//do the search for matches
AzureClaimProvider.AzureClaims.UniqueClaimValueCollection results =
SearchClaims(IDENTITY_CLAIM, searchPattern, context.AbsoluteUri);
//if there was more than zero results, add them to the picker
if (results.UniqueClaimValues.Count() > 0)
{
foreach (AzureClaimProvider.AzureClaims.UniqueClaimValue cv in results.UniqueClaimValues)
{
//node where we'll stick our matches
Microsoft.SharePoint.WebControls.SPProviderHierarchyNode matchNode = null;
//get a picker entity to add to the dialog
PickerEntity pe = GetPickerEntity(cv.ClaimValue, cv.ClaimType, SPClaimEntityTypes.User, cv.DisplayName);
//add the node where it should be displayed too
if (!searchTree.HasChild(cv.ClaimType))
{
//create the node so we can show our match in there too
matchNode = new
SPProviderHierarchyNode(ProviderInternalName,
cv.DisplayName, cv.ClaimType, true);
//add it to the tree
searchTree.AddChild(matchNode);
}
else
//get the node for this team
matchNode = searchTree.Children.Where(theNode =>
theNode.HierarchyNodeID == cv.ClaimType).First();
//add the match to our node
matchNode.AddEntity(pe);
}
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
In FillSearch I’m calling my SearchClaims method again. For each UniqueClaimValue I get back (if any), I look to see if I’ve added the claim type to the results hierarchy node. Again, in this case I’ll always only return one claim type (email), but I wrote this to be extensible so you could use more claim types later. So I add the hierarchy node if it doesn’t exist, or find it if it does. I take the PickerEntity that I create created from the UniqueClaimValue and I add it to the hierarchy node. And that’s pretty much all there is too it.
I’m not going to cover the FillSchema method or any of the four Boolean property overrides that every custom claim provider must have, because there’s nothing special in them for this scenario and I’ve covered the basics in other posts on this blog. I’m also not going to cover the feature receiver that’s used to register this custom claims provider because – again – there’s nothing special for this project and I’ve covered it elsewhere. After you compile it you just need to make sure that your assembly for the custom claim provider as well as custom CASI Kit component is registered in the Global Assembly Cache in each server on the farm, and you need to configure the SPTrustedIdentityTokenIssuer to use your custom claims provider as the default provider (also explained elsewhere in this blog).
That’s the basic scenario end to end. When you are in the SharePoint site and you try and add a new user (email claim really), the custom claim provider is invoked first to get a list of supported claim types, and then again as you type in a value in the type in control, or search for a value using the people picker. In each case the custom claims provider uses the custom CASI Kit control to make an authenticated call out to Windows Azure to talk to our WCF, which uses our custom data classes to retrieve data from Azure table storage. It returns the results and we unwrap that and present it to the user. With that you have your complete turnkey SharePoint and Azure “extranet in a box” solution that you can use as is, or modify to suit your purposes. The source code for the custom CASI Kit component, web part that registers the user in an Azure queue, and custom claims provider is all attached to this posting. Hope you enjoy it, find it useful, and can start to visualize how you can tie these separate services together to create solutions to your problems. Here’s some screenshots of the final solution:
Root site as anonymous user:
Here’s what it looks like after you’ve authenticated; notice that the web part now displays the Request Membership button:
Here’s an example of the people picker in action, after searching for claim values that starts with “sp”:
Comments
- Anonymous
January 01, 2003
thanks - Anonymous
January 01, 2003
The comment has been removed - Anonymous
September 18, 2014
The comment has been removed