Adding Custom Claims to the SAML Response – (How to Write a Custom Claim Handler for WSO2 Identity Server)
Overview
The latest release of WSO2 Identity Server (version 5.0.0), is armed with an “application authentication framework” which provides lot of flexibility in authenticating users from various service providers who are using heterogeneous protocols. It has several extension points, which can be used to cater several customized requirements commonly found in enterprise systems. With this post, I am going to share the details on making use of one such extension point.
Functionality to be Extended
When SAML Single Sign On is used in enterprise systems it is through the SAML Response that the relying party get to know whether the user is authenticated or not. At this point relying party is not aware of other attributes of the authenticated user which it may need for business and authorization purposes. To provide these attribute details for the relying party, SAML specification has allowed to send attributes as well in the SAML Response. WSO2 Identity Server supports this out of the box via the GUI provided for administrators. You can refer [1] for the details on this functionality and configuration details.
The flexibility provided by this particular extension, comes handy when we have a requirement to add additional attributes to the SAML Response, apart from the attributes available in the underline user store. There may be external data sources we need to look, in order to provide all the attributes requested by the relying parties.
In the sample I am to describe here, we will be looking into a scenario where the system needs to provide some local attributes of the user which are stored in user store, with some additional attributes I expect to be retrieved from an external data source.
Following SAML Response is what we need to send to the relying party from WSO2 IS.
<saml2p:Response Destination="https://localhost:9444/acs" ID="faibaccbcepemkackalbbjkihlegenhhigcdjbjk" InResponseTo="kbedjkocfjdaaadgmjeipbegnclbelfffbpbophe" IssueInstant="2014-07-17T13:15:05.032Z" Version="2.0" xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <saml2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">localhost </saml2:Issuer> <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> .......... </ds:Signature> <saml2p:Status> <saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/> </saml2p:Status> <saml2:Assertion ID="phmbbieedpcfdhcignelnepkemobepgaaipbjjdk" IssueInstant="2014-07-17T13:15:05.032Z" Version="2.0" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <saml2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">localhost</saml2:Issuer> <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> ......... </ds:Signature> <saml2:Subject> <saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">Administrator</saml2:NameID> <saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"> <saml2:SubjectConfirmationData InResponseTo="kbedjkocfjdaaadgmjeipbegnclbelfffbpbophe" NotOnOrAfter="2014-07-17T13:20:05.032Z" Recipient="https://localhost:9444/acs"/> </saml2:SubjectConfirmation> </saml2:Subject> <saml2:Conditions NotBefore="2014-07-17T13:15:05.032Z" NotOnOrAfter="2014-07-17T13:20:05.032Z"> <saml2:AudienceRestriction> <saml2:Audience>carbonServer2</saml2:Audience> </saml2:AudienceRestriction> </saml2:Conditions> <saml2:AuthnStatement AuthnInstant="2014-07-17T13:15:05.033Z"> <saml2:AuthnContext> <saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml2:AuthnContextClassRef> </saml2:AuthnContext> </saml2:AuthnStatement> <saml2:AttributeStatement> <saml2:Attribute Name="http://wso2.org/claims/role" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"> <saml2:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string"> Internal/carbonServer2,Internal/everyone </saml2:AttributeValue> </saml2:Attribute> <saml2:AttributeStatement> <saml2:Attribute Name="http://pushpalanka.org/claims/keplerNumber" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"> <saml2:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string"> E90836W19881010 </saml2:AttributeValue> </saml2:Attribute> <saml2:Attribute Name="http://pushpalanka.org/claims/status" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"> <saml2:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string"> active </saml2:AttributeValue> </saml2:Attribute> </saml2:AttributeStatement> </saml2:AttributeStatement> </saml2:Assertion> </saml2p:Response>
In this response we are having one local attribute, which is role and two additional attributes http://pushpalanka.org/claims/keplerNumber and http://pushpalanka.org/claims/status which have been retrieved from some other method we can define in our extension.
How?
- Implement the customized logic to get the external claims. There are just two facts we need to note at this effort.
- The custom implementation should either implement the interface ‘org.wso2.carbon.identity.application.authentication.framework.handler.claims.ClaimHandler’ or extend the default implementation of the interface ‘org.wso2.carbon.identity.application.authentication.framework.handler.claims.impl.DefaultClaimHandler’.
- The map returned at the method, ‘public Map<String, String> handleClaimMappings’ should contain all the attributes we want to add to the SAML Response.
Following is the sample code I was written, adhering to the above. The external claims may have been queried from a database, read from a file or using any other mechanism as required.
public class CustomClaimHandler implements ClaimHandler { private static Log log = LogFactory.getLog(CustomClaimHandler.class); private static volatile CustomClaimHandler instance; private String connectionURL = null; private String userName = null; private String password = null; private String jdbcDriver = null; private String sql = null; public static CustomClaimHandler getInstance() { if (instance == null) { synchronized (CustomClaimHandler.class) { if (instance == null) { instance = new CustomClaimHandler(); } } } return instance; } public Map<String, String> handleClaimMappings(StepConfig stepConfig, AuthenticationContext context, Map<String, String> remoteAttributes, boolean isFederatedClaims) throws FrameworkException { String authenticatedUser = null; if (stepConfig != null) { //calling from StepBasedSequenceHandler authenticatedUser = stepConfig.getAuthenticatedUser(); } else { //calling from RequestPathBasedSequenceHandler authenticatedUser = context.getSequenceConfig().getAuthenticatedUser(); } Map<String, String> claims = handleLocalClaims(authenticatedUser, context); claims.putAll(handleExternalClaims(authenticatedUser)); return claims; } /** * @param context * @return * @throws FrameworkException */ protected Map<String, String> handleLocalClaims(String authenticatedUser, AuthenticationContext context) throws FrameworkException { .... } private Map<String, String> getFilteredAttributes(Map<String, String> allAttributes, Map<String, String> requestedClaimMappings, boolean isStandardDialect) { .... } protected String getDialectUri(String clientType, boolean claimMappingDefined) { .... } /** * Added method to retrieve claims from external sources. This results will be merged to the local claims when * returning final claim list, to be added to the SAML response, that is sent back to the SP. * * @param authenticatedUser : The user for whom we require claim values * @return */ private Map<String, String> handleExternalClaims(String authenticatedUser) throws FrameworkException { Map<String, String> externalClaims = new HashMap<String, String>(); externalClaims.put("http://pushpalanka.org/claims/keplerNumber","E90836W19881010"); externalClaims.put("http://pushpalanka.org/claims/status","active"); return externalClaims; } }
- Drop the compiled OSGI bundle at IS_HOME/repository/components/dropins. (We developed this as a OSGI bundle as we need to get local claims as well using RealmService. You can find the complete bundle and source code here)
- Point WSO2 Identity Server to use the new custom implementation we have.
In IS_HOME/repository/conf/security/applicationauthentication.xml configure the new handler name. (in ‘ApplicationAuthentication.Extensions.ClaimHandler’ element.)
<ClaimHandler>com.wso2.sample.claim.handler.CustomClaimHandler</ClaimHandler>
Now if look at the generated SAML Response, we will see the external attributes added.
Cheers!
[1] – https://docs.wso2.com/display/IS500/Adding+a+Service+Provider
Reference: | Adding Custom Claims to the SAML Response – (How to Write a Custom Claim Handler for WSO2 Identity Server) from our JCG partner Pushpalanka at the Pushpalanka’s Blog blog. |
Hi Pushpalanka,
Thanks for the article.. how can we store these custom claims/value into the user store for a Specific user? so that when user comes again, all custom claims will be populated in claims list