How to Allow Only Users Who Have a Community Badge to Your SharePoint 2013 Site

This post will show how you can secure a web site based on if a user has been given a badge in a SharePoint 2013 community site.

Badges and Reputation in SharePoint 2013 Community Sites

SharePoint 2013 Community Sites provide a new set of features to implement game theory, or gamification.  One feature is called “reputation”, where participation earns you points.  As you earn points, you earn achievements that are reflected as your reputation.  The settings for reputation allow you to define your own points system, defining the points earned for creating a post, replying to a post, having a post marked with 4 or 5 stars or “liked”, and having a post marked as “Best Reply”.  You further define the number of points per level, and whether the achievements are represented as an image or as text.

image

The second feature to point out is called a Gifted Badge.  The site owner will define a set of badges that allow you to define the name of each badge.

image

This allows a community site owner to define a set of badges and assign them to community members.  For instance, you might want to create a badge that calls out if someone has achieved a particular certification level.  Just highlight a community site member and the “Give Badge” button will be enabled in the ribbon in the Moderation tab.

image

Once users have been assigned badges, they appear on the front page of the Community site in the “Top contributors” web part.

image

 

When I first saw the new Community Sites in SharePoint 2013, I was excited.  Of course, there are a ton of things that I would like to see improved, but this is a decent start.  To me, the ability to assign badges seemed really cool and I saw a bunch of opportunities such as assigning badges when employees are on-boarded and they complete training courses.  Out of the box, all you can do with them is either earn them or assign them.  What would be really cool is if you could somehow use them to protect content such that only people who have a specific badge are allowed to use a site.  For instance, I could create a badge called “Microsoft Certified Professional” and only users who have that badge are allowed into the “MCPs Only” web site.  That would allow me to do this:

image

That is exactly what we are going to build.

Wanna Come In?  Show Me Your Badge!

Badges are simply items in a list, so how would you use that in order to allow or deny someone access to a site?  The solution is a custom claims provider.   A claims provider is used to augment a user’s claims and to provide name resolution.  Augmenting claims simply means adding more claims into the user’s token.  For instance, I can add a claim that says what the user’s favorite basketball team is, what their hair color is, whatever information I want to add to their user token.  Here is a screen shot where I added a new claim type “https://Microsoft.PFE.Claims/Badges” with a value “Microsoft Certified Master”.

image

When the user logs in, our custom claim provider will augment the user’s claim set by adding an additional claim indicating if the user has been assigned a badge.  If the user has not been assigned a badge, then no claim is added.

This scenario could be adapted for many other scenarios.  Perhaps you have a document library that contains your company’s intellectual property, and only people who have authored intellectual property have access to a special site.  Maybe you require people to complete training courses before allowing them access to content.  There are many ways that you can adapt this scenario.

The Badge Custom Claims Provider

There are a number of claim providers already registered out of the box, and you can register more than one claim provider to augment claims.  Using the Get-SPClaimProvider cmdlet in PowerShell, we can see the claim providers that have been registered.

image

Start by creating a new SharePoint 2013 farm solution using the “SharePoint 2013 – Empty Project” template.  Next, add a class that derives from Microsoft.SharePoint.Administration.Claims.SPClaimProvider.  Here is a class diagram of what we will implement.  

image

Before we go into the claims provider, let’s get our data access layer stuff out of the way.

Reading Badges From a Community Site

The first step is to create a few classes that store the data we want to work with.  The first class is Badge that just holds the ID and Title for the badge.

 namespace Microsoft.PFE.ClaimsProviders
{
    public class Badge
    {
        public int ID { get; set; }
        public string Title { get; set; }
    }
}

The next class is BadgeUser, which holds information about the user and the badges that they have been assigned.

 using System.Collections.Generic;

namespace Microsoft.PFE.ClaimsProviders
{
    public class BadgeUser
    {
        public string Name { get; set; }
        public string LoginName { get; set; }
        public List<Badge> Badges { get; set; }
    }
}

Now that we’ve defined some classes to hold the data, let’s create a helper class that can fill them with data.  The first method allows us to search for any badges that start with a set of characters.

  1: using Microsoft.SharePoint;
  2: using System.Collections.Generic;
  3:  
  4: namespace Microsoft.PFE.ClaimsProviders
  5: {
  6:     public class BadgeHelper
  7:     {   
  8:         /// <summary>
  9:         /// Get the list of badges that start with the partial name.
  10:         /// </summary>
  11:         /// <param name="partialBadgeName">The characters the badge
  12:         /// name should start with.
  13:         /// </param>
  14:         /// <returns>A list of badges that match the criteria.</returns> 
  15:         public static List<Badge> GetBadgesByPartialName(string partialBadgeName)
  16:         {
  17:  
  18:             List<Badge> badges = new List<Badge>();
  19:  
  20:             //Use RWEP here because the user may not have 
  21:             //access to the site.
  22:             SPSecurity.RunWithElevatedPrivileges(delegate()
  23:             {
  24:                 using (SPSite site = new SPSite("https://intranet.contoso.local"))
  25:                 {
  26:                     SPWeb web = site.OpenWeb("Community");
  27:                     
  28:                     SPQuery query = new SPQuery();
  29:                     query.Query = "<Where>" +
  30:                                        "<BeginsWith>" +
  31:                                            "<FieldRef Name='Title' />" +
  32:                                            "<Value Type='TEXT' >" + 
  33:                                                partialBadgeName + 
  34:                                            "</Value>" +
  35:                                        "</BeginsWith>" + 
  36:                                    "</Where>";
  37:  
  38:                     query.ViewFields = "<FieldRef Name='ID'/>" +
  39:                                         "<FieldRef Name='Title'/>";
  40:                                         
  41:                     SPList badgeList = web.Lists["Badges"];
  42:                     SPListItemCollection items = badgeList.GetItems(query);
  43:  
  44:                     
  45:                     if (null != items)
  46:                     {
  47:                         foreach (SPListItem item in items)
  48:                         {
  49:                             badges.Add(new Badge 
  50:                             { 
  51:                                 ID = item.ID, 
  52:                                 Title = item.Title 
  53:                             });
  54:                         }
  55:                     }
  56:                     
  57:                 }
  58:             });
  59:             return badges;
  60:         }

The call on line 22 is necessary because the current user may not have access to the community site that we are obtaining data from.  You can see this is a pretty simple method that just gets data from a SharePoint list. The next method looks nearly identical in that it gets a list of data based on an exact match rather than a partial match.

         /// <summary>
        /// A list of badges that match the exact name.
        /// </summary>
        /// <param name="name">The name of the badge to match.</param>
        /// <returns>A list of badges that have the exact name.</returns>
        public static List<Badge> GetBadgesByExactName(string name)
        {            
            List<Badge> badges = new List<Badge>();

            //Use RWEP here because the user may not have 
            //access to the site.
            SPSecurity.RunWithElevatedPrivileges(delegate()
            {
                using (SPSite site = new SPSite("https://intranet.contoso.local"))
                {
                    SPWeb web = site.OpenWeb("Community");

                    SPQuery query = new SPQuery();
                    query.Query = "<Where>" +
                                       "<Eq>" + 
                                            "<FieldRef Name='Title' />" +
                                            "<Value Type='TEXT' >" + 
                                                name + 
                                            "</Value>" +
                                       "</Eq>" +
                                   "</Where>";
                    query.ViewFields = "<FieldRef Name='ID'/>" +
                                        "<FieldRef Name='Title'/>";

                    SPList badgeList = web.Lists["Badges"];
                    SPListItemCollection items = badgeList.GetItems(query);


                    if (null != items)
                    {
                        foreach (SPListItem item in items)
                        {
                            badges.Add(new Badge
                            {
                                ID = item.ID,
                                Title = item.Title
                            });
                        }
                    }

                }
            });
            return badges;
        }

The last method bears a little more explanation.  Now we want to get the badges for a particular user.  The user may not have access to the site, so we have to use RunWithElevatedPrivileges.  In fact, the user may not even be in the site users list, so we have to use EnsureUser in order to obtain a valid reference to the SPUser object for that user. 

  1:         /// <summary>
  2:         /// Gets the badges for a particular user
  3:         /// </summary>
  4:         /// <param name="loginName">The login name in encoded claims
  5:         /// format. For instance, "i:0#.w|contoso\administrator".</param>
  6:         /// <returns>The user information and badges for that user.</returns>
  7:         public static BadgeUser GetBadgesForUser(string loginName)
  8:         {
  9:             BadgeUser badgeUser = null;
  10:  
  11:             //Use RWEP here because the user may not have 
  12:             //access to the site.
  13:             SPSecurity.RunWithElevatedPrivileges(delegate()
  14:             {
  15:                 using (SPSite site = new SPSite("https://intranet.contoso.local"))
  16:                 {
  17:                     SPWeb web = site.OpenWeb("Community");
  18:  
  19:                     //The encoded claim will be in the format
  20:                     //0#.f|ldapmember|kirkevans. Add the i: prefix
  21:                     //to indicate this is an identity claim.
  22:                     if (!loginName.StartsWith("i:"))
  23:                     {
  24:                         loginName = "i:" + loginName;
  25:                     }
  26:  
  27:                     //EnsureUser will only work if the same login provider
  28:                     //is available in this site. For example, if you are
  29:                     //calling this from a user authenticated in another web 
  30:                     //application, and this site does not have FBA configured,
  31:                     //the call to EnsureUser will fail.
  32:                     SPUser user = user = web.EnsureUser(loginName);
  33:  
  34:                     SPQuery query = new SPQuery();
  35:                     query.Query = "<Where>" +
  36:                                     "<And>" +
  37:                                         "<Eq>" +
  38:                                            "<FieldRef Name='Member' LookupId='TRUE' />" +
  39:                                            "<Value Type='USER' >" + user.ID + "</Value>" +
  40:                                         "</Eq>" +
  41:                                         "<IsNotNull>" +
  42:                                            "<FieldRef Name='GiftedBadgeLookup'/>" +
  43:                                         "</IsNotNull>" +
  44:                                     "</And>" +
  45:                                   "</Where>";
  46:  
  47:                     query.ViewFields = "<FieldRef Name='Title'/>" +
  48:                                         "<FieldRef Name='Member'/>" +
  49:                                         "<FieldRef Name='GiftedBadgeLookup'/>";
  50:                     SPList memberList = web.Lists["Community Members"];
  51:                     SPListItemCollection items = memberList.GetItems(query);
  52:  
  53:                     if (items.Count > 0)
  54:                     {
  55:                         badgeUser = new BadgeUser
  56:                         {
  57:                             LoginName = user.LoginName,
  58:                             Name = items[0].Title
  59:                         };
  60:                         List<Badge> badges = new List<Badge>();
  61:  
  62:                         foreach (SPListItem item in items)
  63:                         {
  64:                             SPFieldLookupValue badgeName =
  65:                                 new SPFieldLookupValue(item["GiftedBadgeLookup"].ToString());
  66:                             //The current data structure only allows
  67:                             //one badge, but we may want to add badges
  68:                             //from another system in the future.
  69:                             badges.Add(new Badge
  70:                             {
  71:                                 ID = badgeName.LookupId,
  72:                                 Title = badgeName.LookupValue
  73:                             });
  74:                         }
  75:                         badgeUser.Badges = badges;
  76:                     }
  77:                 }
  78:             });
  79:             return badgeUser;
  80:         }
  81:     }
  82: }

The call to EnsureUser requires that the provider used to authenticate the user is available to this web application.  If we are in one web application that uses FBA claims and then we make a call to another web site (in this case https://intranet.contoso.local), then both web applications must provide the same authentication capabilities.  If one web application uses FBA, the site you are calling EnsureUser for must also be configured to use the same FBA provider.

Implementing the Badge Claim Provider

Now that we have the data access stuff out of the way, let’s focus on the really cool part of the solution.  We’ll start by implementing the properties as shown in the previous class diagram.

  1: using System;
  2: using System.Collections.Generic;
  3: using Microsoft.SharePoint.Administration;
  4: using Microsoft.SharePoint.Administration.Claims;
  5: using Microsoft.SharePoint.WebControls;
  6:  
  7: namespace Microsoft.PFE.ClaimsProviders
  8: {
  9:     public class BadgeClaimProvider : SPClaimProvider
  10:     {
  11:         #region Constructor
  12:         public BadgeClaimProvider(string displayName)
  13:             : base(displayName)
  14:         {
  15:         }
  16:         #endregion
  17:  
  18:         #region Properties
  19:         internal static string ProviderInternalName
  20:         {
  21:             get { return "BadgeClaimsProvider"; }
  22:         }
  23:  
  24:         public override string Name
  25:         {
  26:             get { return ProviderInternalName; }
  27:         }
  28:  
  29:         internal static string ProviderDisplayName
  30:         {
  31:             get { return "Badges for User"; }
  32:         }
  33:  
  34:         private static string BadgeClaimType
  35:         {
  36:             get { return "https://Microsoft.PFE.Claims/Badges"; }
  37:         }
  38:  
  39:         private static string BadgeClaimValueType
  40:         {
  41:             get { return Microsoft.IdentityModel.Claims.ClaimValueTypes.String; }
  42:         }
  43:  
  44:         public override bool SupportsEntityInformation
  45:         {
  46:             get { return true; }
  47:         }
  48:  
  49:         public override bool SupportsHierarchy
  50:         {
  51:             get { return false; }
  52:         }
  53:  
  54:         public override bool SupportsResolve
  55:         {
  56:             get { return true; }
  57:         }
  58:  
  59:         public override bool SupportsSearch
  60:         {
  61:             get { return true; }
  62:         }
  63:  
  64:         #endregion

 

The ProviderDisplayName property on line 29 can be seen in the DisplayName property when we use the Get-SPClaimProvider cmdlet.  The BadgeClaimType (line 34) defines the URI for the claim, and the BadgeClaimValueType (line 39) defines the type of value the claim will contain. 

The four properties starting with “Support” indicate to SharePoint what capabilities this provider has. 

  • EntityInformation – provides claims augmentation for a specific user.
  • Hierarchy – used in the People Picker to create a hierarchical representation of claims.
  • Resolve – Find an exact match for the claim value. 
  • Search – Search for a value based on a partial word or search pattern.

Our provider will support entity information, search, and resolve. 

Claims Augmentation - Providing Entity Information

We need to add a claim to the user’s token that shows a badge that they might have earned.  To do that, we first tell SharePoint that we are going to fill a claim with a specific type (“https://Microsoft.PFE.Claims/Badges”) and what type its value will be (Microsoft.IdentityModel.Claims.ClaimValueTypes.String).  We then use the FillClaimsForEntity method to augment the claims for the current user.  This method accepts a parameter, “entity”, the value of which is the user’s identity claim (for instance, “0#.f|ldapmember|kirkevans”).  We then pass that value to our helper method GetBadgesForUser.

  1:         protected override void FillClaimTypes(List<string> claimTypes)
  2:         {
  3:             if (claimTypes == null)
  4:                 throw new ArgumentNullException("claimTypes");
  5:  
  6:             // Add our claim type.
  7:             claimTypes.Add(BadgeClaimType);
  8:         }
  9:  
  10:         protected override void FillClaimValueTypes(List<string> claimValueTypes)
  11:         {
  12:             if (claimValueTypes == null)
  13:                 throw new ArgumentNullException("claimValueTypes");
  14:  
  15:             // Add our claim value type.
  16:             claimValueTypes.Add(BadgeClaimValueType);
  17:         }
  18:  
  19:  
  20:         protected override void FillClaimsForEntity(Uri context, SPClaim entity, List<SPClaim> claims)
  21:         {            
  22:             if (entity == null)
  23:                 throw new ArgumentNullException("entity");
  24:  
  25:             if (claims == null)
  26:                 throw new ArgumentNullException("claims");
  27:  
  28:                         
  29:             var badgeUser = BadgeHelper.GetBadgesForUser(entity.Value);
  30:  
  31:             
  32:             if (null != badgeUser)
  33:             {
  34:                 if (null != badgeUser.Badges)
  35:                 {
  36:                     foreach (var badge in badgeUser.Badges)
  37:                     {
  38:                         claims.Add(CreateClaim(BadgeClaimType, badge.Title, BadgeClaimValueType));
  39:                     }   
  40:                 }
  41:             }                     
  42:         }
  43:         
  44:         
  45:  
  46:         protected override void FillEntityTypes(List<string> entityTypes)
  47:         {
  48:             if (null == entityTypes)
  49:             {
  50:                 throw new ArgumentNullException("entityTypes");
  51:             }
  52:             entityTypes.Add(SPClaimEntityTypes.FormsRole);            
  53:         }

Enabling the People Picker

So far, all we have done is add a claim to the user’s token.  We want to enable the scenario where someone can create a site, select Users and Groups, and use the People Picker to select our badge, allowing any users with that badge access to the site.  To do this, we need to implement a few more methods: FillSchema, FillSearch and FillResolve.  FillSchema tells the PeoplePicker what information we will display and in what format.  FillSearch accepts a string value that we use to find any badges that start with those characters.

         protected override void FillSchema(SPProviderSchema schema)
        {
            if (null == schema)
            {
                throw new ArgumentNullException("schema");
            }

            schema.AddSchemaElement(new SPSchemaElement(PeopleEditorEntityDataKeys.DisplayName,
                "Display Name", SPSchemaElementType.TableViewOnly));
        }

        protected override void FillSearch(Uri context, 
            string[] entityTypes, 
            string searchPattern, 
            string hierarchyNodeID, 
            int maxCount, 
            SPProviderHierarchyTree searchTree)
        {
            if (!EntityTypesContain(entityTypes, SPClaimEntityTypes.FormsRole))
            {
                return;
            }

            List<Badge> badges = BadgeHelper.GetBadgesByPartialName(searchPattern);
            if(null != badges)
            {
                foreach (var badge in badges)
                {
                    PickerEntity entity = CreatePickerEntity();
                    entity.Claim = CreateClaim(BadgeClaimType, badge.Title, BadgeClaimValueType);
                    entity.Description = badge.Title;
                    entity.DisplayText = badge.Title;
                    entity.EntityData[PeopleEditorEntityDataKeys.DisplayName] = badge.Title;
                    entity.EntityType = SPClaimEntityTypes.FormsRole;
                    entity.IsResolved = true;
                    searchTree.AddEntity(entity);
                }
            }
        }

Another way you can use the People Picker is to type a value in.  When you type the value instead of using search, this calls the FillResolve method of the registered claims providers.

 

         protected override void FillResolve(Uri context, 
            string[] entityTypes, 
            SPClaim resolveInput, 
            List<PickerEntity> resolved)
        {
            FillResolve(context, entityTypes, resolveInput.Value, resolved);
        }

        protected override void FillResolve(Uri context, 
            string[] entityTypes, 
            string resolveInput, 
            List<PickerEntity> resolved)
        {
            List<Badge> badges = BadgeHelper.GetBadgesByExactName(resolveInput);
            if (null != badges)
            {
                foreach (var badge in badges)
                {
                    PickerEntity entity = CreatePickerEntity();
                    entity.Claim = CreateClaim(BadgeClaimType, badge.Title, BadgeClaimValueType);
                    entity.Description = badge.Title;
                    entity.DisplayText = badge.Title;
                    entity.EntityData[PeopleEditorEntityDataKeys.DisplayName] = badge.Title;
                    entity.EntityType = SPClaimEntityTypes.FormsRole;
                    entity.IsResolved = true;
                    resolved.Add(entity);
                }
            }
        }

        protected override void FillHierarchy(Uri context, 
            string[] entityTypes, 
            string hierarchyNodeID, 
            int numberOfLevels, 
            SPProviderHierarchyTree hierarchy)
        {
            throw new NotImplementedException();
        }
    }
}

Registering Your Custom Claim Provider

In Visual Studio 2012, add a new farm-scoped feature and a feature receiver.  Change the type of the feature receiver to SPClaimProviderFeatureReceiver.  You only need to override a few properties here, no need to override FeatureActivating or FeatureDeactivating.

 using System;
using System.Runtime.InteropServices;
using Microsoft.SharePoint.Administration.Claims;

namespace Microsoft.PFE.ClaimsProviders.Features.BadgeClaimFeature
{
    /// <summary>
    /// This class handles events raised during feature activation, deactivation, installation, uninstallation, and upgrade.
    /// </summary>
    /// <remarks>
    /// The GUID attached to this class may be used during packaging and should not be modified.
    /// </remarks>

    [Guid("99871a2c-f81a-443c-9625-d2465d2ec29c")]
    public class BadgeClaimFeatureEventReceiver : SPClaimProviderFeatureReceiver
    {


        public override string ClaimProviderAssembly
        {
            get { return typeof(BadgeClaimProvider).Assembly.FullName; }
        }

        public override string ClaimProviderDescription
        {
            get { return "A sample provider written by Kirk Evans"; }
        }

        public override string ClaimProviderDisplayName
        {
            get { return BadgeClaimProvider.ProviderDisplayName; }
        }

        public override string ClaimProviderType
        {
            get { return typeof(BadgeClaimProvider).FullName; }
        }
    }
}

Right-click the project and choose Publish.  This will package the solution into a WSP.  Once packaged into a WSP, you can add the solution and deploy it using PowerShell.

 Add-SPSolution -LiteralPath C:\temp\claims\Microsoft.PFE.ClaimsProviders.wsp
Install-SPSolution microsoft.pfe.claimsproviders.wsp  -GACDeployment

Securing Content Based on a Badge

Now let’s put our new claim provider through its paces. I create a new site titled “MCM Only” with unique permissions.  I then determine who to add to the group “MCM Only Members”.  When I search for “Microsoft” I see a list of the badges that start with the search term (this called our FillSearch method).  When we hover over a result, a popup shows which claim provider the information came from.

image

Now let’s try our FillResolve method by typing the value in. 

image

When we hover over the underlined value, we choose the value that matches.

image

Once we choose a value, only users that have that claim will be granted access.  For those users that have access to the site because they match a claim, they will be granted access.  Notice that it shows in the top nav bar for a user who has the badge.

image

When I try to access the site, it succeeds.

image

Further, SharePoint is smart enough to security trim away the site that a user doesn’t have access to based on our claim provider!  Now I am logged in as Dan Jump, who does not have a badge.

image

And when I try to access the site by typing it in the URL, it fails just as we would expect.

image

Summary

Custom claim providers address a large number of scenarios that people commonly face with SharePoint, enabling the ability to add additional information into the user’s token and then using that information to secure content or assign work to users via the people picker.  There are quite a few interesting ideas on using custom claim providers.

Securing SharePoint Content using Profile Attributes

Leveraging Facebook for SharePoint Security

Creating a Claims Provider Based on SharePoint Audiences

Creating a Hierarchical Claim Provider Based on Favorite Basketball Teams

As I survey many of the implementations that have been done for various customers using a mix of HttpModules and code in master pages to provide less than elegant solutions to similar problems, I can see more and more scenarios that could be solved using custom claim providers.

Microsoft.PFE.ClaimsProviders.zip