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 therequestedClaims
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 STSpom.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.
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 theclaims: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 theonBehalfOf
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; }
No comments:
Post a Comment