01 October 2014

STS Claim Mappings using JEXL Scripts

Before CXF version 2.7.13 it was quite difficult to use claim mappings in the STS, because CXF did not provide any generic claim mapping solution but instead required custom Java code for each claim mapping. Beginning of version 2.7.13 (not yet released) CXF comes with a JexlClaimsMapper which allows to define claim mappings at configuration time with Java Expression Language (JEXL).
Also a new feature in CXF which goes hand in hand with the JexlCaimsMapper is a special ClaimUtils class providing methods for common claim handling tasks.

In this blog I'll write about:
  • How to setup claim mappings in the STS
  • Basic JEXL Claim Handling
  • Several JEXL Claim Mapping Samples

STS Claim Mapping Setup

The best and fastest way to setup a claim mapping scenario in my opinion is to use the CXF Fediz HelloWorld demonstrator. You can also take a look at my previous blog where I described how to setup the demo app, as well as the IDP and STS. Of course you can omit the Kerberos related steps.

Once you have the basic HelloWorld Sample up and running, I'll explain how to switch from identity mapping to a claim mapping next.

IDP Setup

First of all you need to tell the IDP in realm B that you need claims for realm A. To do so you must set the requestedClaims property in your idp-realmA bean in idp-config-realmb.xml. Your result should look like the following:
<beans >
    . . . 
    <bean id="idp-realmA" class="org.apache.cxf.fediz.service.idp.model.ServiceConfig">
        <property name="realm" value="urn:org:apache:cxf:fediz:idp:realm-A" />
        <property name="protocol" value="http://docs.oasis-open.org/wsfed/federation/200706" />
        <property name="serviceDisplayName" value="Resource IDP Realm A" />
        <property name="serviceDescription" value="Resource IDP Realm A" />
        <property name="role" value="SecurityTokenServiceType" />
        <property name="tokenType" value="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0" />
        <property name="lifeTime" value="3600" />
        <property name="requestedClaims">
            <util:list>
                <bean class="org.apache.cxf.fediz.service.idp.model.RequestClaim">
                    <property name="claimType" value="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname" />
                    <property name="optional" value="false" />
                </bean>
                <bean class="org.apache.cxf.fediz.service.idp.model.RequestClaim">
                    <property name="claimType" value="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" />
                    <property name="optional" value="false" />
                </bean>
                <bean class="org.apache.cxf.fediz.service.idp.model.RequestClaim">
                    <property name="claimType" value="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" />
                    <property name="optional" value="false" />
                </bean>
                <bean class="org.apache.cxf.fediz.service.idp.model.RequestClaim">
                    <property name="claimType" value="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role" />
                    <property name="optional" value="false" />
                </bean>                                                
            </util:list>
        </property>
    </bean>
</beans>

In your idp-config-realma.xml you need to change the federationType of your trusted-idp-realmB bean from FederateIdentity to FederateClaims:

Since version 1.2 of Fediz the federationType value has changed to FEDERATE_CLAIMS respectively FEDERATE_IDENTITY
<beans >
    . . . 
    <bean id="trusted-idp-realmB" class="org.apache.cxf.fediz.service.idp.model.TrustedIDPConfig">
        <property name="realm" value="urn:org:apache:cxf:fediz:idp:realm-B" />
        <property name="cacheTokens" value="true" />
        <property name="url" value="https://localhost:${realmB.port}/fediz-idp-remote/federation" />
        <property name="certificate" value="realmb.cert" />
        <property name="trustType" value="PEER_TRUST" />
        <property name="protocol" value="http://docs.oasis-open.org/wsfed/federation/200706" />
        <property name="federationType" value="FederateClaims" />
        <property name="name" value="REALM B" />
        <property name="description" value="IDP of Realm B" />
    </bean>
</beans>

STS Setup

In case that your STS pom.xml file does not include the JEXL dependency you can either add this dependency to your STS or alternatively you can just add this library to your Tomcat lib folder.
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-jexl</artifactId>
    <version>2.1.1</version>
</dependency>
Next you need to update the STS of your relaying party IDP (Realm-A). Here you need to modify the relationship between REALMA and REALMB. For this scenario it would be sufficient to update only the second releationship in the cxf-transport.xml file, but to make the relationship homogeneous I'll set both relationships type from FederateIdentity to FederateClaims. In addition to that I need to add my claimsMapper bean with the JexlClaimsMapper implementation and set the claimsMapper instead of the identityMapper in my relationships.
<beans >
    . . . 
    <bean id="claimsMapper" class="org.apache.cxf.sts.claims.mapper.JexlClaimsMapper">
        <constructor-arg value="claimMapping.script" />
    </bean>
    
    <util:list id="relationships">
        <bean class="org.apache.cxf.sts.token.realm.Relationship">
            <property name="sourceRealm" value="REALMA" />
            <property name="targetRealm" value="REALMB" />
            <property name="claimsMapper" ref="claimsMapper" />
            <property name="type" value="FederatedClaims" />
        </bean>
        <bean class="org.apache.cxf.sts.token.realm.Relationship">
            <property name="sourceRealm" value="REALMB" />
            <property name="targetRealm" value="REALMA" />
            <property name="claimsMapper" ref="claimsMapper" />
            <property name="type" value="FederatedClaims" />
        </bean>
    </util:list>
</beans>

At last you need to add your claimMapping.script (could be any name you want) to your WEB-INF folder of your STS (or any other location that you chose for your JexlClaimsMapper). The content of the claimMapping.script will be discussed in the following sections.

Basic JEXL Claim Handling

The following variables will be available within your custom JEXL script and can be used to determine the outcome of the claim mapping process:
  • sourceClaims
    is a ClaimCollection containing all claims provided from the requester IDP/STS (e.g. REALMB) at which the user was authenticated first (e.g. via username/password).
  • targetClaims
    is a ClaimCollection in which you need to add all claims that you want to be available after the claim mapping for your target application.
  • sourceRealm
    Realm ID of the requester IDP/STS (e.g. REALMB)
  • targetRealm
    Realm ID of the relaying party IDP/STS (e.g. REALMA)
  • claimsParameters
    ClaimsParameter containing additional context information like the STS issuer name and applies to address.
JEXL also supports registration of custom classes to be used within your script. This makes it possible to transfer complex code to a Java class and thus simplifying your script code. By default the ClaimUtils class will be available via the following syntax: claims:<methodname>(<parameter>...). This util class provides several convenience methods to simplify claim handling like getting claims from a specific type out of a ClaimCollection, mapping claim values, etc. You will find several samples in the following section.

The last statement in your script should always return the targetClaims!

Each claim can contain an issuer and an originalIssuer attribute. By calling the

claims:updateIssuer(targetClaims, "new issuer")

method you can set your current STS as the issuer of the targetClaims as well as setting the originalIssuer (in your targetClaims) which was the issuer in your sourceClaims. Since the claimsParameters are also available within your JEXL script you can set the current STS issuer name in your claims via claimsParameters.stsProperties.issuer instead of using a static String like "new issuer".

Claim Mapping Samples

In this section I'll show you some common scenarios that you might need, when you want to operate your IDP/STS in claim mapping mode.

Copy All

If all claims regardless of the type and value shall be copied from the original SAML token into the newly generated token the following expression would be sufficient:
{
    // Update claim issuer 
    targetClaims = claims:updateIssuer(sourceClaims, claimsParameters.stsProperties.issuer);

    // Return all claims
    return targetClaims;
}

Copy Roles Only

A simple JEXL script to copy all roles from the source SAML token which was contained in the onBehalfOf token request to the requested target SAML token could look like the following:
{ 
 // Get all role claims 
 var roleClaimType = 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role'; 
 var roleClaim = claims:get(sourceClaims, roleClaimType); 

 // Copy role claims for new token 
 targetClaims = claims:add(targetClaims, roleClaim); 

 // Update claim issuer 
 targetClaims = claims:updateIssuer(targetClaims, claimsParameters.stsProperties.issuer); 

 // Return new claims 
 return targetClaims; 
}

Role Value Filter

The following sample can be used in a scenario when your source role claim contains all role values comma separated in a single claim value and you want the target claim to contain distinct role values which match a specific regular expression (e.g. start with 'ROLE_'):
{ 
    // Get role claim 
    var roleClaimType = 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role'; 
    var roleClaim = claims:get(sourceClaims, roleClaimType);

    // Split multi role values
    roleClaim = claims:multiToSingleValue(roleClaim, ",");

    // Filter role values
    roleClaim = claims:filterValues(roleClaim, "ROLE_.*");

    // Add mapped role claims for new token 
    targetClaims = claims:add(targetClaims, roleClaim); 

    // Update claim issuer 
    targetClaims = claims:updateIssuer(targetClaims, claimsParameters.stsProperties.issuer); 

    // Return new claims 
    return targetClaims; 
} 

Map Role Claims

The following sample shows a role mapping with a custom map (originalRoleName => newRoleName):
{ 
    // Role value mapping 
    var roleClaimType = 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role'; 
    var roleClaim = claims:get(sourceClaims, roleClaimType); 
    var roleMappings = { "admin" : "administrator", "manager" : "manager" }; 
    var mappedRoles = claims:mapValues(roleClaim, roleMappings, false); 

    // Add mapped role claims for new token 
    targetClaims = claims:add(targetClaims, mappedRoles); 

    // Update claim issuer 
    targetClaims = claims:updateIssuer(targetClaims, claimsParameters.stsProperties.issuer); 

    // Return new claims 
    return targetClaims; 
} 

Claim Merges

It is also possible to define script variables to improve readability. The following sample shows a many-too-one claim mapping whereby the first and last name of a person is transformed into a fullname claim, as well as copying all email addresses from the source SAML token:
{ 
    // Merge firstname and lastname to fullname claim 
    var delimiter = ' '; 
    var firstNameClaimType = 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname'; 
    var lastNameClaimType = 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname'; 
    var fullNameClaimType = 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'; 
    var fullNameClaim = claims:merge(sourceClaims, fullNameClaimType, delimiter, firstNameClaimType, lastNameClaimType); 

    // Simple claim copy 
    var emailClaimType = 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/mail'; 
    var emailClaim = claims:get(sourceClaims, emailClaimType); 

    // Add fullname claim and email claim for new token 
    targetClaims = claims:add(targetClaims, fullNameClaim, emailClaim); 

    // Update claim issuer 
    targetClaims = claims:updateIssuer(targetClaims, claimsParameters.stsProperties.issuer); 

    // Return new claims 
    return targetClaims;
}

More complex Sample

The following sample shows a more "real life" sample of how roles claims are transferred from one domain to another by changing the role claim type, filtering for app specific roles, mapping one to many and normalizing the result:
{
    // Get roles
    var sourceRoleClaimType = 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role';
    var roleClaims = claims:get(sourceClaims, sourceRoleClaimType);

    // Update role claim type
    var targetRoleClaimType = 'http://schemas.mycompany.com/security/authorization/claims/role';
    roleClaims = claims:setType(roleClaims, targetRoleClaimType);
    
    // Normalize role claim values
    roleClaims = claims:singleToMultiValue(roleClaims, ",");

    // Application role filter
    roleClaims = claims:filterValues(roleClaims, 'AppName_[a-zA-Z_]+');

    // Map role claims
    var roleMappings = { 
        "AppName_Agent"             : "AppName_User, Agent",
        "AppName_Broker_DE"         : "AppName_User, AppName_Broker",
        "AppName_Partner_Agents"    : "AppName_User, AppName_Partner, External"
        };
    roleClaims = claims:mapValues(roleClaims, roleMappings, false);
    roleClaims = claims:singleToMultiValue(roleClaims, ", ");

    // Remove duplicates
    if (roleClaims != null) {
        var distinctValues = new("java.util.LinkedHashSet", roleClaims.values);
        roleClaims.values.clear();
        roleClaims.values.addAll(distinctValues);
    }

    // Collect claims for new token
    if (roleClaims != null && roleClaims.values.size() > 0) {
        targetClaims = claims:add(targetClaims, roleClaims);
    }

    // Set correct issuer
    targetClaims = claims:updateIssuer(targetClaims, claimsParameters.stsProperties.issuer);

    // Return new claims
    return targetClaims;
} 

Plain JEXL Only

If you do not want to use any util classes in your JEXL script you can also script more complex cases directly within your script. The following sample shall give you just an idea on who this could look like:
{ 
    // Role value mapping
    var roleClaimType = 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role';
    var roleMappings = { "admin" : "administrator", "manager" : "manager" };
    
    for (c : sourceClaims) {
      if(c.claimType == roleClaimType) {
        var mappedValues = new("java.util.ArrayList");
        for (v : c.values) {
          v = v.toUpperCase();
          var newValue = roleMappings.get(v);
          if (newValue != null) {
            mappedValues.add(newValue);
          }
        }
        c.values = mappedValues;
        targetClaims.add(c);
      }
    }

    // Set correct issuer
    for (c : targetClaims) {
      if(c.originalIssuer == null) {
        c.originalIssuer = c.issuer;
      }
      c.issuer = claimsParameters.stsProperties.issuer;
    }

    // Return new claims 
    return targetClaims;
}

4 comments: