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.xsltOnly 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
-
Syncing Transformation Engine configuration to other Nodes
-
String Manipulation
Example 1 — Pass-through (no transformation)
Example 2 — Pad a value
Example 3 — Append a domain suffix
Example 4 — Concatenating
Example 5 — Extract a substring
Example 6 — Remove special characters
-
Conditional Logic & Group-Based Operations
Example 7 — Replace a value based on a condition
Example 8 — Populate a field only when a condition is met
Example 9 — Map group membership to a static value
-
Extension Methods
Example 10 — Case conversion
Example 11 — Base64-encode a claim value
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.
- Decrypt the web.config for the realm.
- Add the following key anywhere inside the <appSettings> section:
<add key="TransformationDebug" value="True" />- 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><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:
- Stop the FileSync service on the Primary node.
- On the Primary node, open D:\SecureAuth\SecureAuth<realm>\paths.list in a text editor.
- Add the following line as the second-from-last line, just above the closing </list> tag:
<path name="PostAuthData\usersprofiledata.xslt" />- Save the file.
- 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.
Comments
Please sign in to leave a comment.