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

27 comments :

  1. This comment has been removed by a blog administrator.

    ReplyDelete
  2. This comment has been removed by a blog administrator.

    ReplyDelete
  3. This comment has been removed by a blog administrator.

    ReplyDelete
  4. Hallo Pushpalanka, I need to know, what is wrong with my CustomUserStore implementation, when SamlSSO doesn't want to retrieve Claims for user from my CustomUserStore. I have this CustomUserStore implemented as JDBC type and mapped to domain name (MYDOMAIN).

    Claims are filled through WSO2 Identity server administration pages, write & read works fine. But when user from this custom user store (MYDOMAIN) is being logged in through SAML SSO, he doesn't get any claim. But If user from PRIMARY domain is logged in, he gets everything OK.

    Can you guide me where to find a problem?

    ReplyDelete
  5. I know, that my previous question is not directly related to this topic, but I would like to kindly ask for answer, if possible. Thank you in advance.

    ReplyDelete
    Replies
    1. Hi Pepa,

      Please refer this Stackoverflow question and answer (http://stackoverflow.com/questions/26850997/wso2-identity-server-5-0-0-fails-to-return-user-claims-in-samlresponse-for-user).

      If that does not help you, we should look at the custom implementation, whether it properly query and get back the results for user claims(UM_USER_ATTRIBUTE table in default schema).

      Thanks,
      Pushpalanka

      Delete
    2. Dear Pushpalanka, I think you've just made my day :) It works now. Who would have known that such small amount of code could do such investigation case. Have a nice day!


      Best regards, Pepa

      Delete
  6. Dear Pushpalanka,

    please, excuse me when I'm using this channel to solve my issues with WSO2 IS, but I think I have another issue related to claims. It is described here:
    http://stackoverflow.com/questions/27355228/wso2-is-5-0-0-claims-mapping-error

    Can you help me resolve this?

    Thank you in advance, Pepa

    ReplyDelete
  7. Hi,

    Can you please let me know how I can handle claims depending upon the SP. Means like I want to return the user role depending upon service provider frm where the request is coming.

    ReplyDelete
  8. Hi, How we can get DOB, Tiltle, PostalCode in SAML Post? Because my User Store from User Profile doesn't give me that option to save those values in order to make it available in SAML Post. Please guide.

    ReplyDelete
    Replies
    1. You can add them as new claim mappings, then those will be available at this level.
      Refer : https://docs.wso2.com/display/IS500/Adding+New+Claim+Mapping

      Delete
  9. Hello Pushpalanka,

    Is it possible to have a request parameter as a part of the SAML Post to identify targer_url? If yes, could you please explain it or provide any link / example etc to support your answer?

    Thanks,
    Anjana

    ReplyDelete
  10. Hello Pushpalanka,
    In my use case I want to log in through an external IDP via a federated authenticator. My credentials are authenticated against their user store, and I want to add claims retrieved from my user store as attributes in the SAML response to my application. In this case, according to my tests any custom claims handler shall not be invoked.

    My next attempt was to customize the federated authenticator (SAMLSSOAuthenticator) itself but I ran into further problems (http://stackoverflow.com/questions/33256571/wso2-identity-server-customize-samlssoauthenticator).

    Any advice would be much appreciated.
    Thanks,
    //John.

    ReplyDelete
  11. hi Pushpalanka,
    i have one question about wso2 IS. Hope you can help me.
    i want to add a new button on IS login page, when click on this button, it will redirect to my app to authenticate the user. after authenticate the user successfully, my app then redirect the user to the service provider's consumer url.

    thanks in advance.
    Xin~

    ReplyDelete
  12. Hi Chin,

    This sounds more like an IDP initiated SAML SSO flow. Usually we do not customize IS management console login page and guess you are talking about IS dashboard login page.. If there is a specfic problem you face, would able to do some help.
    Thanks,
    Pushpalanka

    ReplyDelete
  13. Hi Pushpalanka,
    I mean a SP initiated the SAML SSO, when sp sends the SAML request, i was redirected to the authencationpoint login page, what i want is to customize this login page(add one button as one option to link to my own app to authenticate the user), i want to implement multiple way login on this page.

    thanks,

    ReplyDelete
    Replies
    1. Hi Xin,

      Yes that is allowed. You can go ahead and change the login.jsp file within the authenticationendpoint. This link will help, https://docs.wso2.com/display/IS500/Customizing+Login+Pages

      Delete
  14. Hi Pushpalanka,
    i checked the link you provided, the content is not i want. The description below which comes from the is500 documention is what i want to implement, do you have any samples or some guides to me? thanks!

    Multi-option authentication
    The service provider can define how to authenticate users at the Identity Server, for authentication
    requests initiated by it. While doing that, each service provider can pick more than one authenticators -
    so, the end user will get multiple login options. This can be a combination of local authenticators and
    federated authenticators.

    ReplyDelete
    Replies
    1. This will help you https://docs.wso2.com/display/IS500/Configuring+Local+and+Outbound+Authentication+for+a+Service+Provider
      You don't have to go and change the login page. Based on the above configuration you do, WSO2 IS provides the login page with the above options to users..

      Delete
  15. Hello. I have the same use case as John. Any solution for that..?
    Thanks and best regards.
    Francesco

    ReplyDelete
    Replies
    1. I am also trying to do the same thing as you and John. Did you ever find any info?

      Delete
    2. If it's possible for you to make a mapping between federated userID and local user ID manually as in [1], this can be supported out of the box. Else we can have a custom claim retriever written to get and inject all the claims to SAML response. Let me know if you need further details.

      [1] - https://docs.wso2.com/display/IS530/Associating+User+Accounts

      Delete
    3. yes the federated user name (Which I have set as the subject) and the local userprincipalname do match. I did associate my login with the federated idp but the only way I can make it work is to have a 2 step authentication on the SP and then I have to still login twice.

      Delete
    4. Here is my overall setup. I have sharepoint pointed to a wso2 SP with incoming authentication as WS-Federation. i also have Azure AD setup as an external IdP. I have the authentication of the SP set to Federation and pointing to Azure AD. When I browse to sharepoint it directs me to Azure to login and that works but I also need to have attributes from the local PRIMARY user store to be sent back to sharepoint without having to also login again to the local basic authentication. so I need to somehow match up the user name from Azure to the local PRIMARY user store and get attributes to send back to sharepoint. Both the Azure username and the local username match.

      Delete
  16. Hi Pushpalanka, IT's really a nice article, I wanted to know do we have any provision to make settings to get HL7 compliant claims in SAML response?

    ReplyDelete
    Replies
    1. Yes, wso2 IS has a claim mapping functionality to where we can map local claims to any other format we wish to. Please refer https://docs.wso2.com/display/IS530/Configuring+Claims+for+a+Service+Provider

      Delete