How to Get All User Claims at Claims Augmentation Time in SharePoint 2010

A fairly constant hurdle when doing claims augmentation in SharePoint 2010 has been trying to figure out what claims a user has when your custom claims provider is invoked to do claims augmentation. For example, the claims you want to augment for a person may depend on the value of other claims the user has, e.g. if the person belongs to the role “Domain Admins” then add the role claim “Uber User”, otherwise add the claim “Standard User”. After being frustrated by this for a long time, my good friend Israel V. and Matt Long finally came up with the solution to this sticky problem (and deserves 100% of the credit for this solution, so thank you guys!).

 

One of the underlying problems in trying to get to this information outside of the parameters provided when your claim provider is invoked for augmentation is that you don’t have any access to an HttpContext to look at the collection of claims. Israel and Matt correctly figured this out and figured out the alternative, which is the OperationContext. So what is that?

 

Well, OperationContext has a good overview here: https://msdn.microsoft.com/en-us/library/system.servicemodel.operationcontext(v=VS.90).aspx. In a nutshell, the thing we care about, is it allows you to access incoming message headers and properties (for SAML users) and the security context (for Windows users). So how does this help us? Well, when your custom claims provider is invoked for augmentation, you can get to this incoming message info for SAML users which looks like this:

 

<s:Envelope xmlns:s="https://www.w3.org/2003/05/soap-envelope" xmlns:a="https://www.w3.org/2005/08/addressing">

   <s:Header>

       <a:Action s:mustUnderstand="1">https://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue</a:Action>

       <a:MessageID>urn:uuid:85a0daaa-2288-4d0a-bda8-5fac05ea61cf</a:MessageID>

       <a:ReplyTo>

          <a:Address>https://www.w3.org/2005/08/addressing/anonymous</a:Address>

       </a:ReplyTo>

       <a:To s:mustUnderstand="1">https://localhost:32843/SecurityTokenServiceApplication/securitytoken.svc</a:To>

   </s:Header>

   <s:Body>

       <trust:RequestSecurityToken xmlns:trust="https://docs.oasis-open.org/ws-sx/ws-trust/200512">

          <wsp:AppliesTo xmlns:wsp="https://schemas.xmlsoap.org/ws/2004/09/policy">

              <a:EndpointReference>

                 <a:Address>https://fc1/</a:Address>

              </a:EndpointReference>

          </wsp:AppliesTo>

          <trust:KeyType>https://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer</trust:KeyType>

          <trust:OnBehalfOf>

              <saml:Assertion MajorVersion="1" MinorVersion="1" AssertionID="_8f1d7b46-2b71-4263-859b-c3e358d7ea84" Issuer="https://myadfsserver/adfs/services/trust" IssueInstant="2011-03-26T18:51:54.671Z" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion">

                 <saml:Conditions NotBefore="2011-03-26T18:51:33.198Z" NotOnOrAfter="2011-03-26T19:51:33.198Z">

                     <saml:AudienceRestrictionCondition>

                        <saml:Audience>urn:sharepoint:fc1</saml:Audience>

                     </saml:AudienceRestrictionCondition>

                 </saml:Conditions>

                 <saml:AttributeStatement>

                     <saml:Subject>

                        <saml:SubjectConfirmation>

                        <saml:ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:bearer</saml:ConfirmationMethod>

                        </saml:SubjectConfirmation>

                     </saml:Subject>

                     <saml:Attribute AttributeName="emailaddress" AttributeNamespace="https://schemas.xmlsoap.org/ws/2005/05/identity/claims">

                        <saml:AttributeValue>testuser@vbtoys.com</saml:AttributeValue>

                     </saml:Attribute>

                     <saml:Attribute AttributeName="role" AttributeNamespace="https://schemas.microsoft.com/ws/2008/06/identity/claims">

                        <saml:AttributeValue>pOregon Marketing</saml:AttributeValue>

   <saml:AttributeValue>Domain Users</saml:AttributeValue>

                        <saml:AttributeValue>pSales</saml:AttributeValue>

                        <saml:AttributeValue>Portal People</saml:AttributeValue>

                     </saml:Attribute>

                     <saml:Attribute AttributeName="windowsaccountname" AttributeNamespace="https://schemas.microsoft.com/ws/2008/06/identity/claims">

                        <saml:AttributeValue>testuser</saml:AttributeValue>

                     </saml:Attribute>

                     <saml:Attribute AttributeName="primarysid" AttributeNamespace="https://schemas.microsoft.com/ws/2008/06/identity/claims">

                        <saml:AttributeValue>testuser@vbtoys.com</saml:AttributeValue>

                     </saml:Attribute>

                 </saml:AttributeStatement>

                 <saml:AuthenticationStatement AuthenticationMethod="urn:federation:authentication:windows" AuthenticationInstant="2011-03-26T18:51:33.069Z">

                     <saml:Subject>

                        <saml:SubjectConfirmation>

                        <saml:ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:bearer</saml:ConfirmationMethod>

                        </saml:SubjectConfirmation>

                     </saml:Subject>

                 </saml:AuthenticationStatement>

                 <ds:Signature xmlns:ds="https://www.w3.org/2000/09/xmldsig#">

                     <ds:SignedInfo>

                        <ds:CanonicalizationMethod Algorithm="https://www.w3.org/2001/10/xml-exc-c14n#">

                        </ds:CanonicalizationMethod>

                        <ds:SignatureMethod Algorithm="https://www.w3.org/2001/04/xmldsig-more#rsa-sha256">

                        </ds:SignatureMethod>

                        <ds:Reference URI="#_8f1d7b46-2b71-4263-859b-c3e358d7ea84">

                           <ds:Transforms>

                               <ds:Transform Algorithm="https://www.w3.org/2000/09/xmldsig#enveloped-signature">

                               </ds:Transform>

                               <ds:Transform Algorithm="https://www.w3.org/2001/10/xml-exc-c14n#">

                               </ds:Transform>

                           </ds:Transforms>

                           <ds:DigestMethod Algorithm="https://www.w3.org/2001/04/xmlenc#sha256">

                           </ds:DigestMethod>

                           <ds:DigestValue>5Qvu+blahblah=</ds:DigestValue>

                        </ds:Reference>

                     </ds:SignedInfo>

                     <ds:SignatureValue>VUSrynYjN8NOcUexqJOCblahblah</ds:SignatureValue>

                     <KeyInfo xmlns="https://www.w3.org/2000/09/xmldsig#">

                        <X509Data>

                           <X509Certificate>MIIFlzCCBH+gAwIBAgIKHmblahblahblah</X509Certificate>

                        </X509Data>

                     </KeyInfo>

                 </ds:Signature>

              </saml:Assertion>

          </trust:OnBehalfOf>

          <trust:RequestType>https://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</trust:RequestType>

       </trust:RequestSecurityToken>

   </s:Body>

</s:Envelope>

 

<<apologies for what I realize is a long chunk of ugly Xml, but I wanted you to see it>>

 

Now, since we have a chunk of Xml with our claims embedded inside, it’s no big deal to unwrap them and be able to use them during augmentation. Here’s a quick little sample that I wrote to do just that:

using System.Xml;

 

private class IncomingClaim

{

   public string claimType { get; set; }

   public string claimValue { get; set; }

 

   public IncomingClaim(string claimType, string claimValue)

   {

   this.claimType = claimType;

       this.claimValue = claimValue;

   }

}

 

protected override void FillClaimsForEntity(Uri context, SPClaim entity,

            List<SPClaim> claims)

{

//get the request envelope with the claims information

       string rqst =

System.ServiceModel.OperationContext.Current.RequestContext.RequestMessage.ToString();

 

       //create a list to store the results

       List<IncomingClaim> incomingClaims = new List<IncomingClaim>();

                          

       //create an Xml document for parsing the results and load the data

       XmlDocument xDoc = new XmlDocument();

       xDoc.LoadXml(rqst);

 

       //create a new namespace table so we can query

       XmlNamespaceManager xNS = new XmlNamespaceManager(xDoc.NameTable);

 

       //add the namespaces we'll be using

       xNS.AddNamespace("s", "https://www.w3.org/2003/05/soap-envelope");

       xNS.AddNamespace("trust", "https://docs.oasis-open.org/ws-sx/ws-trust/200512");

       xNS.AddNamespace("saml", "urn:oasis:names:tc:SAML:1.0:assertion");

                    

       //get the list of claim nodes

       XmlNodeList xList =

xDoc.SelectNodes("s:Envelope/s:Body/trust:RequestSecurityToken/trust:OnBehalfOf/saml:Assertion/saml:AttributeStatement/saml:Attribute", xNS);

 

       //get the matches

       if ((xList != null) && (xList.Count > 0))

       {

              //enumerate through the matches to get the claim list

              foreach (XmlNode xNode in xList)

              {

                     //get the claim type first

                     string currentClaimType =

                           xNode.Attributes["AttributeNamespace"].Value +

                           "/" + xNode.Attributes["AttributeName"].Value;

 

                     //each claim type will have one to many child nodes with the values

                     foreach (XmlNode claimNode in xNode.ChildNodes)

                     {

                           incomingClaims.Add(new IncomingClaim(currentClaimType,

                                  claimNode.InnerText));

                     }

              }

       }

 

//now you can do whatever you want with the list of claims and finish

//your augmentation

}

 

That’s pretty much it. The code is simple enough and commented out so verbosely that I believe if you look at it, and paste the Xml into a nice Xml editor (like the one that comes with Visual Studio .NET) that it should be pretty clear how it works. No rocket science at this point, just Xml.

 

There is also an interesting side effect you can implement based on this pattern that blog reader Luis A. points out. The set of claims you will get in the RequestMessage includes everything that came from your IP-STS, including any claims that the SharePoint STS will be dropping if there isn’t a corresponding claim mapped. So, if you know which claims SharePoint is going to be dropping and you want to keep them anyways, you can just augment them yourself again with your custom claims provider. Just pull the claim values and types out of the RequestMessage and add them back in.

 

Windows claims users won’t have this message associated with their request, but you can get a WindowsIdentity for them by using the SecurityContext of the OperationContext. From that you can do all the things available with a WindowsIdentity, like getting the list of groups the user belongs to, etc. Here’s an example of that:

 

//do windows

WindowsIdentity wid =

System.ServiceModel.OperationContext.Current.ServiceSecurityContext.WindowsIdentity;

 

//get a reference to the groups to which the user belongs

IdentityReferenceCollection grps = wid.Groups;

 

//enumerate each group (which will be represented as a SID)

//NOTE that this includes ALL groups - builtin, local, domain, etc.

foreach (IdentityReference grp in grps)

{

       //get the plain name of the group

       SecurityIdentifier sid = new SecurityIdentifier(grp.Value);

       NTAccount groupAccount = (NTAccount)sid.Translate(typeof(NTAccount));

       string groupName = groupAccount.ToString();

 

       //for domain groups remove the domain\ prefix from the group

       //name in order to have feature parity with SAML users

       if (groupName.Contains("\\"))

              groupName = groupName.Substring(groupName.IndexOf("\\") + 1);

 

       //add the claim

       incomingClaims.Add(new

IncomingClaim("https://schemas.microsoft.com/ws/2008/06/identity/claims/role",

              groupName));

}

 

You’ll want to make sure you add a using statement for System.Security.Principal to get the WindowsIdentity, IdentityReference, NTAccount, etc. to resolve correctly. Otherwise the code should again be fairly straightforward. I’m just getting the list of groups for the user and putting them in my custom collection of claims as a standard Role claim.

 

Thanks again Israel and Matt for sharing this super valuable information.

How to Get All User Claims at Claims Augmentation Time in SharePoint 2010.docx