Custom Claims Transformation Modules

 

Custom claims transformation modules let you authenticate users based on business logic that you implement. You can transform existing claims and import claims from other identity data sources, such as a SQL Server database that you have created.

There are two processing stages at which custom claims transformation can occur:

  1. Preprocessing. This stage occurs after claims are retrieved from the Account Store (for the Account Role) or from the incoming token (for the Resource Role). At this stage, outgoing claims have not yet been processed, and identity obfuscation has not yet occurred.

  2. Post-processing. At this stage, outgoing claims have been populated, and identity obfuscation has occurred.

    Note

    Identity claims must not be modified in the post-processing stage. Also, you must not remove all identity claims during transformation.

How to Implement Claims Transformation

To implement claims transformation, do the following:

  1. Add a reference to the System.Web.Security.SingleSignOn.ClaimTransforms.dll assembly.

  2. Add a reference to the System.Web.Security.SingleSignOn.Authorization namespace.

  3. Implement the IClaimTransform interface. The following code example shows how to do this:

public class MyTransform
{
    public void TransformClaims(ref SecurityPropertyCollection incomingClaims, 
                                ref SecurityPropertyCollection corporateClaims, 
                                ref SecurityPropertyCollection outgoingClaims, 
                                ClaimTransformStage transformStage, 
                                string issuer, 
                                string target)
    {
    }
}

The incomingClaims parameter contains claims that are received from an account partner. They are transformed into corporate claims by a built-in transformation. Incoming claims should be accessed during preprocessing. They are used on Resource Federation Service (FS-Rs) only. On an Account Federation Service (FS-A), the incomingClaims parameter is null.

The corporateClaims parameter contains claims that are normalized in an organization's namespace. Corporate claims are treated as claims extracted from an identity store. They are stored in the Logon Accelerator Token (LAT) and can be used for user-specific data persistence to avoid future lookups.

The outgoingClaims parameter contains claims that can be accessed in preprocessing or post-processing. They are processed by both FS-As and FS-Rs. They are sent out in the token without additional processing, bypassing outgoing policy rules.

For more information about these parameters, see the TransformClaims method.

Claim Flow

An FS-A has the following claim flow:

  1. An incoming claim arrives from the Account Store and is transformed into a corporate claim.

  2. The claim undergoes the preprocessing transform.

  3. The corporate claim becomes an outgoing claim.

  4. The claim undergoes the post-processing transform.

  5. The claim undergoes identity obfuscation if the receiving partner is privacy-enhanced.

  6. The claim is added to the outgoing token.

An FS-R has the following claim flow:

  1. A claim is extracted from the token and becomes an incoming claim.

  2. The claim undergoes the preprocessing transform.

  3. The incoming claim is transformed into a corporate claim.

  4. The corporate claim becomes an outgoing claim.

  5. The claim undergoes the post-processing transform.

  6. The claim is added to the outgoing token.

How to Deploy a Custom Claims Transformation Module

  1. Copy the assembly to the \adfs\sts\bin directory. It is not necessary to register the assembly in the Global Assembly Cache (GAC).

  2. Configure the assembly with \adfs\sts\bin\web.config.

  3. Open the ADFS MMC snap-in, right-click the TrustPolicy node, and select Properties.

  4. In the Properties window, select the Transform Module tab.

  5. Click the Browse button and select the assembly.

  6. Specify the fully qualified class name of the class that implements IClaimTransform.

Notes

  • Custom claims transformation affects the performance of the Federation Service. Therefore, your transformation code should be optimized and deterministic.

  • Throwing an exception in ASP.NET will result in a "500 Server Error" message which does not provide any information to the user about why they did not authenticate.

  • It is important to make the initialization of the custom claims transformation module as stable as possible, especially if it creates database connections, because it is difficult to debug.

  • Identity claims must not be modified in the post-processing stage. Also, you must not remove all identity claims during transformation.

  • Your custom claims transformation module should be put in \adfs\sts\bin for best handling by ASP.NET. The Federation Service (FS) will restart if the binary is changed, provided that the user interface is not running.

  • Data specific to your custom claims transformation module should be stored in web.config. The FS will restart if the web.config file is changed, which lets the module detect changes to module-specific data.

  • Be sure to use Access Control Lists (ACL) to restrict access to your custom claims transformation module. Also, make sure to back up your module together with other ADFS data, to guarantee appropriate functioning of the FS after a system restore.

Examples

Empty Custom Claims Transformation Module

The following example implements the customs claim transformation module interface (no functionality added):

using System;
using System.Web.Security.SingleSignOn;
using System.Web.Security.SingleSignOn.Authorization;

namespace EmptyClaimTransform
{
    public class ClaimTransform : System.Web.Security.SingleSignOn.IClaimTransform
    {
        public void TransformClaims(ref SecurityPropertyCollection incomingClaims,
            ref SecurityPropertyCollection corporateClaims,
            ref SecurityPropertyCollection outgoingClaims,
            ClaimTransformStage transformStage,
            string issuer,
            string target)
        {
            switch (transformStage)
            {
                case ClaimTransformStage.PreProcessing:
                    break;
                case ClaimTransformStage.PostProcessing:
                    break;
            }
        }
    }
}

Using web.config for Settings

The following example shows how to retrieve settings from the web.config file:

using System;
using System.Configuration;
using System.Xml;
using System.Xml.Serialization;

using System.Web.Security.SingleSignOn;
using System.Web.Security.SingleSignOn.Authorization;

namespace ConfigBasedTransformModule
{
    public class MyTransformHandler : IConfigurationSectionHandler
    {
        public object Create(object parent, object configContext, XmlNode section)
        {
            XmlSerializer xser = new XmlSerializer(typeof(MyTransformSettings));
            XmlNodeReader xin = new XmlNodeReader(section);
            return xser.Deserialize(xin) as MyTransformSettings;
        }
    }

    // This class corresponds to the config section
    // It should have public properties for each configuration entry
    public class MyTransformSettings
    {
        public MyTransformSettings() {}

        // Add public properties with get/set accessors
    }

    // This class is the transform module itself, and will be instantiated by the Federation Service
    public class MyTransform : IClaimTransform
    {
        // This class will be instantiated once by the FS and will persist unless the FS process is recycled
        private static MyTransformSettings m_settings =
                System.Configuration.ConfigurationManager.GetSection("MyTransformSettings") as MyTransformSettings;

        public MyTransform()
        {
        }

        public void TransformClaims(ref SecurityPropertyCollection incomingClaims,
                                    ref SecurityPropertyCollection corporateClaims,
                                    ref SecurityPropertyCollection outgoingClaims,
                                    ClaimTransformStage transformStage,
                                    string strIssuer,
                                    string strTargetURI)
        {
            if (m_settings == null)
                return;

            // Add custom transform code
        }
    }
}

Rules-based Authorization

The following custom claims transformation module deserializes rules settings from the web.config file and selectively blocks users from successfully authenticating based on those rules. This demonstrates how to use the transform module for policy enforcement. Errors thrown from the transform module will propagate to ASP.NET as a "500 Server Error" message which does not provide any information to the user about why they did not authenticate.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Text;
using System.Xml;
using System.Xml.Serialization;

using System.Web.Security.SingleSignOn;
using System.Web.Security.SingleSignOn.Authorization;

namespace CustomAuthorizationRuleModule
{
    ///
    /// AuthorizationRules allows the rules to be stored for each resource partner URI
    /// such that when a rule exists for that URI, a particular group claim is required in
    /// order to successfully issue a token. If the group claim is not present, the message
    /// and URL fields will be used to provide the user with nice message.
    ///
    public class AuthorizationRulesHandler : IConfigurationSectionHandler
    {
        public object Create(object parent, object configContext, XmlNode section)
        {
            XmlSerializer xser = new XmlSerializer(typeof(AuthorizationRulesList));
            XmlNodeReader xin = new XmlNodeReader(section);
            return xser.Deserialize(xin) as AuthorizationRulesList;
        }
    }

    internal class AuthorizationRulesHash
    {
        // Hashtable for storing the rules keyed from the resource partner URI
        private Hashtable m_hashRules = null;
        public Hashtable Rules
        {
            get { return m_hashRules; }
        }

        public AuthorizationRulesHash(AuthorizationRulesList listRules)
        {
            m_hashRules = new Hashtable();
            foreach (AuthorizationRule rule in listRules.Rules)
            {
                m_hashRules.Add(rule.TargetURI, rule);
            }
        }
    }

    public class AuthorizationRulesList
    {
        private List<AuthorizationRule> m_listRules = null;
        public List<AuthorizationRule> Rules
        {
            get { return m_listRules; }
            set { m_listRules = value; }
        }
    }

    public class AuthorizationRule
    {
        private string m_strMessage = "Access to this site is not allowed for this account.";
        public string Message
        {
            get { return m_strMessage; }
            set { m_strMessage = value; }
        }

        private SecurityProperty m_groupClaim = null;
        internal SecurityProperty GroupClaimProperty
        {
            get { return m_groupClaim; }
        }

        public string GroupClaim
        {
            get { return m_groupClaim.Name; }
            set { m_groupClaim = SecurityProperty.CreateGroupProperty(value); }
        }

        private string m_strURL = null;
        public string URL
        {
            get { return m_strURL; }
            set { m_strURL = value; }
        }

        private string m_strTargetURI = null;
        public string TargetURI
        {
            get { return m_strTargetURI; }
            set { m_strTargetURI = value; }
        }
    }

    ///
    /// TransformModuleClass is responsible for checking if a particular organizational
    /// group claim is present in the cookie generated by FS-A for itself. This transform
    /// module is supposed to be deployed ONLY on FS-A. This transform cannot detect by 
    /// itself if it is deployed on FS-R and results would be unpredictable. 
    /// The transform module terminates the scenario giving an appropriate error message
    /// if the group claim is not found.
    /// If the group claim is found, transform module does nothing.
    ///
    public class CustomTransform : IClaimTransform
    {
        AuthorizationRulesHash m_rules = null;

        //Default constructor.
        public CustomTransform()
        {
            AuthorizationRulesList listRules = System.Configuration.ConfigurationManager.GetSection("AuthorizationRulesList")
                                                as AuthorizationRulesList;
            if (listRules != null)
                m_rules = new AuthorizationRulesHash(listRules);
        }

        // TransformClaims is the entry point for the FS to call. It will call this routine several times
        // as different bits of information are gathered. We only care about final processing once corporate
        // claims (those that remain on the FSA in the LAT) are gathered     
        public void TransformClaims(ref SecurityPropertyCollection incomingClaims,
                ref SecurityPropertyCollection corporateClaims,
                ref SecurityPropertyCollection outgoingClaims,
                ClaimTransformStage transformStage,
                string strIssuer,
                string strTargetURI)
        {
            //
            // If the rules have been removed from web.config, there is no processing to do
            //
            if (m_rules == null)
                return;

            //
            // We do processing only in the final (PostProcessing) stage when the FS-A is ready to send out the token
            //
            if (transformStage != ClaimTransformStage.PostProcessing)
                return;

            if (corporateClaims == null)
                return;

            //
            // Find the authorization rule for this target. If it does not exist, allow this user through
            //
            AuthorizationRule rule = m_rules.Rules[strTargetURI] as AuthorizationRule;
            if (rule == null)
                return;

            //
            // If the group claim exists in the corporateClaims, allow this user through
            //
            foreach (SecurityProperty securityProperty in corporateClaims)
            {
                if (securityProperty.Equals(rule.GroupClaimProperty))
                    return;
            }

            //
            // Since the group claim was not found, present the user with a nice message
            //
            throw new ApplicationException(rule.Message);
        }
    }
}

Enumerating and Adding Claims

The following custom claims transformation module demonstrates how to enumerate the claims that are present in incoming claims, corporate claims, and outgoing claims. It also demonstrates how to add claims to these groups. Finally, it shows how to determine whether the transform module is called in the preprocessing stage or the post-processing stage.

using System;
using System.Collections.Generic;
using System.Text;
using System.Web.Security.SingleSignOn;
using System.Web.Security.SingleSignOn.Authorization;

namespace CustomTransformModule
{
    //Custom transform module that you write should implement interface: IClaimTransform
    //defined in System.Web.Security.SingleSignOn namespace found in 
    //System.Web.Security.SingleSignOn.ClaimTransforms.dll

    public class CustomTransformModuleClass : IClaimTransform
    {
        public CustomTransformModuleClass()
        { }

        /*
        TransformClaims() method is defined in IClaimTransform interface. This method
        provides access to Incoming, Corporate and Outgoing claims in the token and 
        cookie. This method is called twice:
        In Pre-processing phase (before the Federation Server does its own claim
        tranformation as defined in the trustpolicy file) and in Post-processing phase 
        (after the Federation Server has finished doing it's claim transformation).
        Third parameter: outgoingClaims will always contain the final set of claims that
        are sent in the token/cookie returned to the client.
        Parameters: 
        transformStage: This parameter is an enum which represents the stage in which 
                        the method is currently being called. It can have value:
                        PreProcessing or PostProcessing.
        issuer: This is the URI of the issuer of the token
        target: This is the URI of the target of the token. 

        */
        public void TransformClaims(ref SecurityPropertyCollection incomingClaims, 
                                    ref SecurityPropertyCollection corporateClaims, 
                                    ref SecurityPropertyCollection outgoingClaims, 
                                    ClaimTransformStage transformStage, 
                                    string issuer, 
                                    string target)
        {


            /*
            //If you want to see what claims are present in incomingClaims 
            displayClaims(incomingClaims);

            //Similarly if you want to see what claims are present in corporateClaims 
            displayClaims(corporateClaims);

            //Similarly if you want to see what claims are present in outgoingClaims 
            displayClaims(outgoingClaims);

            //If you want to add some claims in Incoming claim collection
            addClaims(incomingClaims);

            //Similarly if you want to add some claims in Corporate claim collection.
            addClaims(corporateClaims);

            //Similarly if you want to add some claims in Outgoing claim collection.
            addClaims(outgoingClaims);
            */

               //Checking value of transformStage parameter to determine in which stage
                //is Transform module called.
                if (transformStage == ClaimTransformStage.PreProcessing)
                {                    
                    //Do something you want to do in Pre-Processing phase                    
                }
                else if(transformStage == ClaimTransformStage.PostProcessing)
                {
                    addClaims(outgoingClaims);
                    //Do something you want to do in Post-Processing phase
                }

        }



        //This function demonstrates how to create a UPN, Email, Common Name, Group and Custom claim.
        //It also shows how to add claims in a SecurityPropertyCollection (which can be
        //either of Incoming, Corporate, Outgoing claim collection set).
        private void addClaims(SecurityPropertyCollection securityPropertyCollection)
        {
            //Adding a upn claim to the collection.
            string upn = "user@microsoft.com";
            securityPropertyCollection.Add(SecurityProperty.CreateUserPrincipalNameProperty(upn));

            // create an email claim and add it to the collection
            string emailAddr = "user@msn.com";
            securityPropertyCollection.Add(SecurityProperty.CreateEmailNameProperty(emailAddr));

            // create a common name claim and add it to the collection
            string commonName = "Test user";
            securityPropertyCollection.Add(SecurityProperty.CreateCommonNameProperty(commonName));

            // create a group claim and add it to the collection
            string group = "Happy Testers";
            securityPropertyCollection.Add(SecurityProperty.CreateGroupProperty(group));

            // Creating a new custom cadding a custom claim to the collection
            string claimName = "ssn";
            string claimValue = "123-45-6789";
            securityPropertyCollection.Add(SecurityProperty.CreateCustomClaimProperty(claimName,claimValue));


       }


        //This function demonstrates how to enumerate claims in a SecurityPropertyCollection 
        //(which can be either of Incoming, Corporate, Outgoing claim collection set).
        private void displayClaims(SecurityPropertyCollection securityPropertyCollection)
        {
            if (null == securityPropertyCollection)
            {
                Console.WriteLine("SecurityPropertyCollection is null.");
                return;
            }

            foreach (SecurityProperty securityProperty in securityPropertyCollection)
            {                                   
                if(securityProperty.ClaimType.Equals(WebSsoClaimType.Custom))
                {                 
                    Console.WriteLine("Custom claim name: {0} - value: {1}",
                                            securityProperty.Name, securityProperty.Value);
                }


                else if (securityProperty.ClaimType.Equals(WebSsoClaimType.Upn))
                {
                    Console.WriteLine("UPN - {0}", securityProperty.Value);
                }
                else if (securityProperty.ClaimType.Equals(WebSsoClaimType.Email))
                {
                    Console.WriteLine("Email - {0}", securityProperty.Value);
                }
                else if (securityProperty.ClaimType.Equals(WebSsoClaimType.CommonName))
                {
                    Console.WriteLine("Common Name - {0}", securityProperty.Value);
                }
                //If the Uri matches GroupPropertyExtension.Uri then it is group claim.
                else if (securityProperty.ClaimType.Equals(WebSsoClaimType.Group))
                {
                    Console.WriteLine("Group - {0}", securityProperty.Value);
                }
            }
        }
    }
}