How To: Transformation Engine - Common XSLT Script Examples

Follow
    Applies to:
  • SecureAuth Identity Platform
Deployment model:
  • Cloud
  • Hybrid
  • On Premises
  • Version Affected: All

     

    Overview

    The SecureAuth Transformation Engine uses XSLT 1.0 to modify or augment profile data before it is sent in an assertion or token. This article provides a reference collection of common script patterns — from basic string manipulation to conditional logic and group handling — which can be combined to meet most integration requirements.

    For prerequisites and how to enable the Transformation Engine, refer to the Transformation Engine Guide.

    Service providers frequently require claim values in a format that differs from how attributes are stored in Active Directory - different case, a prefix or suffix appended, a substring extracted, or a derived value based on group membership. 
    The built-in Profile Properties mapping handles direct pass-through only, the Transformation Engine is required for any manipulation before the assertion or token is issued.

    Some examples:

    • CamelCase/lowercase/UPPERCASE
    • A prefix or suffix
    • A substring extracted
    • A derived value based on group membership

    All examples below are written for use inside the <xsl:template match="/"><user> block of the .xslt file saved at:

    D:\SecureAuth\SecureAuth<realm>\PostAuthData\usersprofiledata.xslt
     

    Only the relevant field element(s) are shown in each example. All other entries are assumed to be working as expected (passing through default entries or correctly formatted transformations)
     

    In this article


     

    Debug the Transformation Engine

    To see the exact output the Transformation Engine produces for a given user, enable debug logging. This applies regardless of which example above is in use.

    1. Decrypt the web.config for the realm.
    2. Add the following key anywhere inside the <appSettings> section:
    <add key="TransformationDebug" value="True" />
    1. Save the file.

    From this point on, every time the Transformation Engine runs, a debugoutput.xml file is written to D:\SecureAuth\SecureAuth<realm>\PostAuthData, showing the actual output produced for that request.



     

    Example 1 — Pass-through (no transformation)

    The default template passes all values through unchanged. Use this as your base and modify only the fields that need transformation

      <UserID>
    	<xsl:value-of select="user/UserID" />
      </UserID>
      <Email1>
    	<xsl:value-of select="user/Email1" />
      </Email1>
      <Email2>
    	<xsl:value-of select="user/Email2" />
      </Email2>

    If a Transformation File is broken during updates, a default copy can be found at the below location, simply copy this file over your 'broken' version to return it to a default, 'out of the box' configuration

    D:\SecureAuth\Template\PostAuthData\


     

    String Manipulation

    Example 2 — Pad a value

    Useful when an SP requires a minimum amount of characters sent as a value, for example a Numeric UserId 10 characters long yet UserIds are only 6 characters long in the Datastore, therefore they are required to be 'padded'
    In the below example, the UserId is sent within the AuxID1 Profile Property if the UserID is 123456 it will be Transformed to 0000123456

    <AuxID1>
    	<xsl:value-of select="format-number(user/AuxID1, '0000000000')" />
    </AuxID1>


     

    Example 3 — Append a domain suffix to UserID

    Useful when the SP requires a UPN-style value but the directory stores only the sAMAccountName.
    In the below example, a user with a UserID of 'TestUser' will send a value of TestUser@domain.com in the Email1 field

    <Email1>
    	<xsl:value-of select="concat(user/UserID, '@domain.com')" />
    </Email1>


     

    Example 4 — Concatenating (Combining multiple fields)

    Combining two or more values into a single Property
    In the below example, the FirstName, a space, and the LastName values are joined together

    <AuxID1>
    	<xsl:value-of select="concat(user/FirstName, ' ', user/LastName)"/>
    </AuxID1>


     

    Example 5 — Extract a substring

    Substring() requires at least 2 values and no more than 3.

    Substring(%value%, %starting_character%)
    - This will take the value provided and start the extraction at the specified start_character/index to the end of the value

    Substring(%value%, %starting_character%, %count_characters%)
    - This will take the value provided and start the extraction at the specified starting_character/index and count how many indexed characters to extract from that point


    Examples
    Extract everything before or after a specific character, for example, extracting a Username from an Email address or a domain value from an Email address

    Extract a UserID from Email1 and pass it as a value as the UserID

    <!-- Extract the local/username part of a UPN (everything before '@') -->
    <UserID>
    	<xsl:value-of select="substring-before(user/Email1, '@')"/>
    </UserID>

    Extract a Domain from Email1 and add it as a value for AuxID1
     
    <!-- Extract the domain part of a UPN (everything after '@') -->
    <AuxID1>
    	<xsl:value-of select="substring-after(user/Email1, '@')"/>
    </AuxID1>

     

    Functions can also be nested within other functions to carry out two transformations in one process. In the below example, the substring-before transformation becomes the value passed to the substring-after transformation

    In the below example, the Transformation Engine is taking the full DN of the Manager's AD Object, extracting the substring of everything before the first comma substring-before(user/AuxID3, ','), which leaves CN=Manager Name.

    Then the Transformation Engine is taking everything after the CN= value substring-after(... ), 'CN='), which leaves Manager Name to be sent as the value for AuxID3 

    <!-- Extract a manager's CN from a full DN stored in AuxID3 
        Input:  CN=Manager Name,OU=IT,OU=Users,DC=example,DC=com     Output: Manager Name -->
    <AuxID3>
    	<xsl:value-of select="substring-after(substring-before(user/AuxID3, ','), 'CN=')"/>
    </AuxID3>

     

    Extract/Strip a fixed-length prefix

    substring(string, position to start from)
    For example, if we had a UserID made up of 2 characters and 6 numbers, but we only wanted to send the numbers as the UserID, the below can be used.
    In the below example, a UserID of AB123456 would become a UserID of 123456.

    <!-- Strip the first 2 characters by starting at the third index entry -->
    <UserID>
      <xsl:value-of select="substring(user/UserID, 3)"/>
    </UserID>


    substring(string, position to start from, index positions to capture)
    Alternatively, we can choose to take only the 4 middle characters of the AB123456 string and end up with 1234.

    <!-- Strip the first and last 2 characters by starting at the third index entry
       and extracting 4 characters/index entries from that point -->
    <UserID>
      <xsl:value-of select="substring(user/UserID, 3, 4)"/>
    </UserID>


     

    Example 6 — Remove special characters from a String

    The double translate() pattern effectively sets a whitelist of characters allowed to be passed through. Only characters explicitly listed will pass through, anything else found in the provided string will be stripped out.
    For example, the below would transform a UserID of d'arcy.smith@example.com to darcy.smith@example.com   

    <!-- Allow only alphanumeric, underscore, dot, and @ in UserID -->
    <UserID>
      <xsl:value-of select="translate(user/UserID, translate(user/UserID, '_@.0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ''), '')"/>
    </UserID>
     

    Adjust the allow-list string to suit the SP's requirements.




     

    Conditional Transformations and Group-Based Operations

    Example 7 — Replace a value based on condition

    Apply a transformation only when the value matches a condition; fall back to the original value otherwise.
    In the below example, we will be looking for any Emails which have a domain of '@olddomain.com' and will be replacing those with '@newdomain.com', any other email domains are left untouched

    <Email1>
    	<xsl:choose>
    		<xsl:when test="contains(user/Email1, '@olddomain.com')">
    			<xsl:value-of select="concat(substring-before(user/Email1, '@olddomain.com'), '@newdomain.com')"/>
    		</xsl:when>
    		<xsl:otherwise>
    			<xsl:value-of select="user/Email1"/>
    		</xsl:otherwise>
    	</xsl:choose>
    </Email1>


     

    Example 8 — Populate a field only when a condition is met

    For example, only populating a value if the Authenticating User is a member of a specific AD Group
    In the below example, if the Authenticating User is a member of the Domain Admins AD Group, their Email2 property will be populated with their Email address (or whichever value populates Email1).

    <Email2>
    	<xsl:if test="contains(current(),'Domain Admins')">
    		<xsl:value-of select="user/Email1"/>
    	</xsl:if>
    </Email2>


     

    Example 9 — Set a static value based on group membership <choose> and <for-each>

    <choose> runs from top to bottom and stops on the first match.

    Use this when a fixed string is required and is to be determined by the user's group
    In this example, an Authenticating User can be a member of both Domain Admins and Helpdesk, but as we have Domain Admins listed nearer the top of the <choose> logic, it will hit first, and the value of DAs will be sent.

    <GroupList>
       <Groups>
    	   <xsl:choose>
    		   <xsl:when test="contains(current(),'Domain Admins')">
    			   <Value>
    				   <xsl:text>DAs</xsl:text>
    			   </Value>
    		   </xsl:when>
    		   <xsl:when test="contains(current(),'Helpde')">
    			   <Value>
    				   <xsl:text>HD</xsl:text>
    			   </Value>
    		   </xsl:when>
    	   </xsl:choose>
       </Groups>
    </GroupList>


    Alternatively, using a <for-each> loop will cause the logic to run through all checks and send through any values which match, so the same Authenticating User as above would have both DomainAdmins and Helpdesk set for the Groups property.

    <GroupList>
    	<Groups>
    		<xsl:for-each select="user/GroupList/Groups/Value">
    			<xsl:if test="contains(current(),'Domain Admins')">
    				<Value>
    					<xsl:value-of select="current()"/>
    				</Value>
    			</xsl:if>
    			<xsl:if test="contains(current(),'Helpde')">
    				<Value>
    					<xsl:value-of select="current()"/>
    				</Value>
    			</xsl:if>
    		</xsl:for-each>
    	</Groups>
    </GroupList>


    If the value of the actual Group is required, simply change the below line
    <xsl:text>.....</xsl:text>

    To
    <xsl:value-of select="current()"/>



     

    Example 10 — Case conversion

    Modifying a value to be all upperCase,all lowerCase or TitleCase

    There is no inbuilt function to upperCase/lowerCase or TitleCase text via the Transformation Engine, so we need to add a function at the top of the file itself, then we can call the required method against whichever profile property we need to.

    Replace the below line - seen at the top of the Transformation Engine file content

    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    With the below content

    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:user="urn:my-scripts">
       <msxsl:script language="C#" implements-prefix="user">
           <msxsl:using namespace="System.Globalization"/>
           <![CDATA[
               public string LowerCase(string v)
               {
                   return v.ToLower();
               }
               public string UpperCase(string v)
               {
                   return v.ToUpper();
               }
               public string TitleCase(string v)
               {
                   TextInfo textinfo = new CultureInfo("en-US",false).TextInfo;
                   return textinfo.ToTitleCase(v);
               }
           ]]>
     </msxsl:script>
     
    The very next line within the Transformation Engine should be <xsl:template match="/">
     
    For the Value/Profile Property required to be altered, simply alter it to the below (change the fieldname to suit)
     
    <xsl:value-of select="user:UpperCase(user/UserID)" />
    OR
    <xsl:value-of select="user:LowerCase(user/UserID)" />
    OR
    <xsl:value-of select="user:TitleCase(user/UserID)" />


     

    Example 11 — Base64-encode a claim value

    Useful when a service provider requires a claim value Base64-encoded but the built-in "Base64 Encoded" option under SAML attributes isn't honored — that option only works reliably over WS-Federation, not SAML.

    Add a b64 extension method alongside any other custom methods at the top of the file, then call it against the profile property that needs encoding. In the example below, AuxID6 is Base64-encoded:

    <msxsl:script language="C#" implements-prefix="user">
        <msxsl:using namespace="System.Globalization"/>
        <![CDATA[
            public string b64(string v)
            {
                var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(v);
                return System.Convert.ToBase64String(plainTextBytes);
            }
        ]]>
    </msxsl:script>
    <AuxID6>
        <xsl:value-of select="user:b64(user/AuxID6)"/>
    </AuxID6>

    If the attribute is a GUID (such as ObjectGUID), use ToByteArray() instead — encoding a GUID as plain text produces the wrong value. Parse it as a Guid first, then encode its raw byte array. Replace the b64 method above with:

    public string b64(string v)
    {
        Guid newGuid = new System.Guid(v);
        Byte[] bytes = newGuid.ToByteArray();
        return System.Convert.ToBase64String(bytes);
    }


     

    Syncing Transformation Engine configuration to other Nodes

    The usersprofiledata.xslt file is not synced by FileSync by default. This applies no matter which example above was used to edit it — a change made on one node isn't reflected on the other nodes in a cluster unless it's copied over manually or FileSync is configured to include the file.

    There are two ways to handle this:

    Manually copy the file — after editing usersprofiledata.xslt on one node, copy it to D:\SecureAuth\SecureAuth<realm>\PostAuthData on every other node in the cluster.

    Or add the file to FileSync's sync list, so future changes sync automatically:

    1. Stop the FileSync service on the Primary node.
    2. On the Primary node, open D:\SecureAuth\SecureAuth<realm>\paths.list in a text editor.
    3. Add the following line as the second-from-last line, just above the closing </list> tag:
    <path name="PostAuthData\usersprofiledata.xslt" />

    paths.list file open in a text editor, showing the added line for PostAuthData\usersprofiledata.xslt highlighted just above the closing list tag.

    1. Save the file.
    2. Start the FileSync service on the Primary node.


     

    SecureAuth Knowledge Base Articles provide information based on specific use cases and may not apply to all appliances or configurations. Be advised that these instructions could cause harm to the environment if not followed correctly or if they do not apply to the current use case.

    Customers are responsible for their own due diligence prior to utilizing this information and agree that SecureAuth is not liable for any issues caused by misconfiguration directly or indirectly related to SecureAuth products.

    0 out of 0 found this helpful

    Comments

    0 comments

    Please sign in to leave a comment.