Thursday, July 31, 2014

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?

1. 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;
    }
}



2.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)

3. Point WSO2 Identity Server to use the new custom implementation we have.

In IS_HOME/repository/conf/security/application­authentication.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

Friday, July 18, 2014

Leveraging federation capabilities of Identity Server for API gateway (First Webinar Conducted by Myself)

The first Webinar conducting experience for me happened on July 02nd 2014, with opportunity given  by WSO2 Lanka (pvt) Ltd, where I am currently employed. As always that was a great opportunity given by the company to me.

The Webinar was done to highlight the capabilities introduced with WSO2 IS 5.0.0, the First Enterprise Identity Bus, which is 100% free and open source. This Webinar, in detail discuss and demonstrate the power and value it adds when these capabilities of federation are leveraged in combination with WSO2 API Manager. 

Following are the slides used at the Webinar. 

The session went under following outline and you can watch the full recording of the session at WSO2 library, 'Leveraging federation capabilities of Identity Server for API gateway'.

  • Configuring WSO2 Identity Server as the OAuth2 key manager of the API Manager
  • Identity federation capability of Identity Server 5.0
  • How to connect existing IAM solution with API Manager through identity bridge
  • How to expand the solution to various other possible requirements
Lot more to improve. Any feed backs, suggestions are warmly welcome!