Tuesday, April 2, 2013

Accessing claim aware services using WSO2 Identity Server STS secured with non-repudiation scenario

Usually, WSO2 Identity Server's (IS) Security Token Service (STS) is secured using UsernameToken. By doing so, claims related to a particular user can be easily retrieved from an user-store.

However, there can be situations where STS is secured using the non-repudiation scenario, in which, client authenticates by signing the Request for Security Token (RST) using his private key. At the STS side, claims should be retrieved based on client's X.509 certificate's Common Name (CN), if the STS trusts the client.

Currently, IS does not have inbuilt support for this scenario. However, using a provided extension point, we can easily plug this behaviour! A custom attribute finder for non-repudiation scenario can be written and given to IS to execute.

In this post I will discuss how to achieve this using WSO2 IS 4.1.0, WSO2 ESB 4.6.0 (which are the latest versions at the time of writing) and an STS Sample, which can be downloaded from here.

sts-sample includes executables and as well as the source files with an eclipse project that was configured using maven.

With the above pre-requisites, we will follow below steps:

1. Add a custom attribute finder to IS.
2. Configure the key stores of Client and IS, so IS will have client's x.509 certificate.
3. Run the servers.
4. Secure ESB's echo service with a custom policy, which will make the service claim aware.
5. Add echo service as a trusted service of STS.
6. Secure STS with the non-repudiation scenario.
7. Add necessary claims to the user.
7. Test the scenario with the STS client.

1. Adding a custom attribute finder to IS

Following is the source code of a custom attribute finder. It simply parses the distinguished name of the certificate and extract the value of CN, which will be used as the identifier to query the user store for claims.

package org.wso2.carbon.identity.resource.sts.attributeservice.x509;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.rahas.RahasData;
import org.apache.rahas.impl.util.SAMLAttributeCallback;
import org.opensaml.SAMLException;
import org.wso2.carbon.identity.provider.AttributeCallbackHandler;
import org.wso2.carbon.identity.provider.IdentityAttributeService;

public class X509AttributeService extends AttributeCallbackHandler implements IdentityAttributeService {

    private static Log log = LogFactory.getLog(X509AttributeService.class);

    public void handle(SAMLAttributeCallback attrCallback) throws SAMLException {
        RahasData data = null;
        String userIdentifier = null;
        String[] splitArr;
    
        try {
                data = attrCallback.getData();
                splitArr = data.getPrincipal().getName().split(",")[0].split("=");
    
                if (splitArr.length == 2) {
                    userIdentifier = splitArr[1];
                    loadClaims(userIdentifier);
                    processClaimData(data, data.getClaimElem());
                    populateClaimValues(userIdentifier, attrCallback);
                }   
        }   
        catch (Exception e) {
            log.error("Error occuerd while populating claim data", e); 
        }   
    }   
}

You can download the compiled version of this - org.wso2.carbon.identity.resource.sts.attributeservice.x509-1.0.0.jar - from here.

Then, copy it to {IS_HOME}/repository/components/dropins folder.

2. Configuring key stores

Following steps will generate a key pair for the particular user you are interested in client's key store, and add his/her certificate to IS' key store.

If you are using the key store of the sts-sample downloaded (which is located at sts-sample/src/main/resources/keystore/wso2carbon.jks), and if you want to test with the "admin" user, skip step 1.

1. Generate a new key pair in client's key store with the CN "admin" (or any other, if you want to test a different user in IS user store).
keytool -genkey -keyalg RSA -alias admin -keypass admin123 -keystore path/to/client/wso2carbon.jks -storepass wso2carbon -dname "CN=admin"
2. Generate a certificate from the key pair
keytool -export -alias admin -file path/to/admin.cert -keystore path/to/client/wso2carbon.jks -storepass wso2carbon
3. Import the new certificate to {IS_HOME}/repository/resources/security/wso2carbon.jks.
keytool -import -alias admin -file path/to/admin.cert -keystore path/to/server/wso2carbon.jks -storepass wso2carbon
When it asks "Trust this certificate? [no]:" at the end of above command, give yes.

3. Running the servers

In ESB, change the "Offset" value to 1 in {ESB_HOME}/repository/conf/carbon.xml. This will allow us to run both IS and ESB servers in parallel. IS will run on default port 9443 and ESB on 9444.

Start both servers by executing...

{IS_HOME}/bin/wso2server.sh and {ESB_HOME}/bin/wso2server.sh on Linux

or

{IS_HOME}/bin/wso2server.bat and {ESB_HOME}/bin/wso2server.bat on Windows.

4. Securing an echo service (i.e. the relying party) at ESB

1. Add a custom policy to the registry.

First we will create a new collection (which is essentially a folder) to maintain custom policies...


...then we will add the service-policy.xml located at 'sts-sample/src/main/resources/' to it.


2. Secure the echo service with the custom policy.

Go to Services list and click "Unsecured" link of echo service...

Select "Yes" for "Enable Security?".

Give the path of the policy file we uploaded to registry, in "Policy From Registry" section at the end of the page. And press "Next".


On the next page tick 'wso2carbon.jks' as the Trusted Key Store.


End service is successfully configured now!

5. Add ESB's echo service as a trusted service of STS

In "Security Token Service" page of IS, 'Add new trusted service' with the following details...

Endpoint Address = http://localhost:8281/services/echo
Certificate Alias = wso2carbon

6. Secure STS with the Non-repudiation scenario

Select "Apply Security Policy" on above screen.

Select "Yes" for "Enable Security?".

Select scenario 2 - Non-repudiation, and press "Next".


On next page, as we did for ESB, select 'wos2carbon.jks' as the trusted key store.

7. Make sure necessary claims are added to the User.

Echo service requires first name and the email address as the claims (refer service-policy.xml).

Check user profile of the particular user ("admin" in default case) to make sure values for those claims are available.



Configuring both ESB and IS is done!

8. Testing with the STS Client

Following are the (partially clipped) sources that make up the Client.

Client.java can invoke token issue binding on STS, as well as send the request to the echo service.

public class Client {

    ...
    
    public static void main(String[] args) {
        Client client = new Client();
        client.run();
    }

    private void run() {
        try {
            loadConfigurations();

            // set the trust store as a system property for communication over
            // TLS.
            System.setProperty("javax.net.ssl.trustStore", keystorePath);
            System.setProperty("javax.net.ssl.trustStorePassword", keystorePwd);

            // create configuration context
            ConfigurationContext configCtx = ConfigurationContextFactory
                    .createConfigurationContextFromFileSystem(repoPath);

            // create STS client
            STSClient stsClient = new STSClient(configCtx);
            stsClient.setRstTemplate(getRSTTemplate());

            String action = null;
            String responseTokenID = null;

            action = TrustUtil.getActionValue(RahasConstants.VERSION_05_02,
                    RahasConstants.RST_ACTION_ISSUE);
            stsClient.setAction(action);

            // request the security token from STS.
            Token responseToken;
            
            Policy stsPolicy = loadPolicy(stsPolicyPath);

            // add rampart config assertion to the ws-sec policies
            RampartConfig rampartConfig = buildRampartConfig();
            stsPolicy.addAssertion(rampartConfig);
            
            responseToken = stsClient.requestSecurityToken(null, stsEPR, stsPolicy, relyingPartyEPR);

            // store the obtained token in token store to be used in future
            // communication.
            TokenStorage store = TrustUtil.getTokenStore(configCtx);
            responseTokenID = responseToken.getId();
            store.add(responseToken);

            // print token
            System.out.println(responseToken.getToken().toString());

            ...

            //Send the token to relying party
            if (enableRelyingParty) {
                /* Invoke secured service using the obtained token */
                OMElement responseElem = null;

                // create service client
                ServiceClient serClient = new ServiceClient(configCtx, null);

                // engage modules
                serClient.engageModule("addressing");
                serClient.engageModule("rampart");

                // load policy of secured service
                Policy sec_policy = loadPolicy(relyingPartyPolicyPath);

                // add rampart config to the ws-sec policies
                sec_policy.addAssertion(rampartConfig);

                // set in/out security policies in client opts
                serClient.getOptions().setProperty(RampartMessageData.KEY_RAMPART_POLICY,
                        sec_policy);

                // Set the token id as a property in the Axis2 client scope, so that
                // this will be picked up when creating the secure message to invoke
                // the endpoint.
                serClient.getOptions().setProperty(RampartMessageData.KEY_CUSTOM_ISSUED_TOKEN,
                        responseTokenID);

                // set action of the Hello Service to be invoked.
                serClient.getOptions().setAction("urn:echoString");
                serClient.getOptions().setTo(new EndpointReference(relyingPartyEPR));

                // invoke the service
                responseElem = serClient.sendReceive(getPayload(echoRequestMsg));
                // cleanup transports
                serClient.getOptions().setCallTransportCleanup(true);

                System.out.println(responseElem.toString());
                
                System.exit(0);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TrustException e) {
            e.printStackTrace();
        } catch (XMLStreamException e) {
            e.printStackTrace();
        }
    }

    private OMElement getRSTTemplate() throws TrustException {
        OMFactory omFac = OMAbstractFactory.getOMFactory();
        OMElement element = omFac.createOMElement(SP11Constants.REQUEST_SECURITY_TOKEN_TEMPLATE);

        if (ClientConstants.SAML_TOKEN_TYPE_20.equals(tokenType)) {
            TrustUtil.createTokenTypeElement(RahasConstants.VERSION_05_02, element).setText(
                    RahasConstants.TOK_TYPE_SAML_20);
        } else if (ClientConstants.SAML_TOKEN_TYPE_11.equals(tokenType)) {
            TrustUtil.createTokenTypeElement(RahasConstants.VERSION_05_02, element).setText(
                    RahasConstants.TOK_TYPE_SAML_10);
        }

        if (ClientConstants.SUBJECT_CONFIRMATION_BEARER.equals(subjectConfirmationMethod)) {
            TrustUtil.createKeyTypeElement(RahasConstants.VERSION_05_02, element,
                    RahasConstants.KEY_TYPE_BEARER);
        } else if (ClientConstants.SUBJECT_CONFIRMATION_HOLDER_OF_KEY
                .equals(subjectConfirmationMethod)) {
            TrustUtil.createKeyTypeElement(RahasConstants.VERSION_05_02, element,
                    RahasConstants.KEY_TYPE_SYMM_KEY);
        }

        // request claims in the token.
        OMElement claimElement = TrustUtil.createClaims(RahasConstants.VERSION_05_02, element,claimDialect);
        // Populate the <Claims/> element with the <ClaimType/> elements
        addClaimType(claimElement, claimUris);

        return element;
    }

    private void addClaimType(OMElement parent, String[] claimUris) {
        OMElement element = null;
        // For each and every claim uri, create an <ClaimType/> elem
        for (String attr : claimUris) {
            element = parent.getOMFactory()
                    .createOMElement(
                            new QName("http://schemas.xmlsoap.org/ws/2005/05/identity",
                                    "ClaimType", "wsid"), parent);
            element.addAttribute(parent.getOMFactory().createOMAttribute("Uri", null, attr));
        }
    }

    private Policy loadPolicy(String policyPath) throws XMLStreamException, FileNotFoundException {
        StAXOMBuilder omBuilder = new StAXOMBuilder(policyPath);
        return PolicyEngine.getPolicy(omBuilder.getDocumentElement());
    }

    private RampartConfig buildRampartConfig() {
        RampartConfig rampartConfig = new RampartConfig();
        rampartConfig.setUser(username);
        rampartConfig.setEncryptionUser(encryptionUser);
        rampartConfig.setUserCertAlias(userCertAlias);
        rampartConfig.setPwCbClass(pwdCallbackClass);

        Properties cryptoProperties = new Properties();
        cryptoProperties.put("org.apache.ws.security.crypto.merlin.keystore.type", "JKS");
        cryptoProperties.put("org.apache.ws.security.crypto.merlin.file", keystorePath);
        cryptoProperties
                .put("org.apache.ws.security.crypto.merlin.keystore.password", keystorePwd);

        CryptoConfig cryptoConfig = new CryptoConfig();
        cryptoConfig.setProvider("org.apache.ws.security.components.crypto.Merlin");
        cryptoConfig.setProp(cryptoProperties);

        rampartConfig.setEncrCryptoConfig(cryptoConfig);
        rampartConfig.setSigCryptoConfig(cryptoConfig);

        return rampartConfig;
    }

    private OMElement getPayload(String value) {
        OMFactory factory = null;
        OMNamespace ns = null;
        OMElement elem = null;
        OMElement childElem = null;

        factory = OMAbstractFactory.getOMFactory();
        ns = factory.createOMNamespace("http://echo.services.core.carbon.wso2.org", "ns");
        elem = factory.createOMElement("echoString", ns);
        childElem = factory.createOMElement("in", null);
        childElem.setText(value);
        elem.addChild(childElem);

        return elem;
    }
    
    ...
}

PasswordCBHandler.java is used by underlying Rampart module to get the password of the key alias which is used to sign the request.

public class PasswordCBHandler implements CallbackHandler{
    
    ...
    
    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {

        readUsernamePasswordFromProperties();
        
        WSPasswordCallback pwcb = (WSPasswordCallback) callbacks[0];
        String id = pwcb.getIdentifier();
        int usage = pwcb.getUsage();

        if (usage == WSPasswordCallback.USERNAME_TOKEN) {

           if (username.equals(id)) {
               pwcb.setPassword(password);
           }
        } else if (usage == WSPasswordCallback.SIGNATURE || usage == WSPasswordCallback.DECRYPT) {

            if (keyAlias.equals(id)) {
                pwcb.setPassword(keyPassword);
            }
        }
    }
    
    ...
}

You can configure the client by using 'sts-sample/src/main/resources/client.properties' file.

By default, it is configured to run the client in SAML2 and 'Bearer' subject confirmation modes using "admin" as the user.

There are scripts named sts-client.sh and sts-client.bat included in the sts-sample download. By using them, you can directly run the client without much hassle.

Upon execution, you will see an output similar to below...


That's it, we have successfully executed the scenario! :-)

2 comments:

@#$#@$#@ said...

I am looking for .net sample (wcf client) to request the STS for a SAML token

dulanja said...

I'm not familiar how this is done in .NET. However, take a look at WIF (Windows Identity Foundation)