22 September 2014

REST Security - SAML Authentication & XACML Authorization

REST is very successful because it is simple and efficient. SOAP on the other hand usually comes with a high overhead but is also well standardized. In respect to security SOAP provides many well defined ways to exchange security token, whereas REST leaves everything up to the service owner/caller.
This becomes a problem regarding interoperability, if every vendor provides its own custom solution. The same is true for CXF. CXF provides three different ways to exchange a SAML token within a REST call, none of them are standardized. The only standard I could find related to REST Services was the SAML ECP Profile, which is not yet implemented for CXF.

In this post, I'm going to write how to setup a demo application to enable SAML user authentication and XACML user authorization. Both can be used best with Talend STS & PDP.

Getting Started with a REST Demo Application

Since I like Maven a lot, I'm going to use a Maven archetype to setup my initial version of my demo app:

mvn archetype:generate -Dfilter=org.apache.cxf.archetype:
 
Select org.apache.cxf.archetype:cxf-jaxrs-service and choose your desired version (I picked 2.7.12 for this blog). Set you own maven group and artifact id, like the follwoing:

Define value for property 'groupId': : org.talend.example.rest
Define value for property 'artifactId': : security-demo
Define value for property 'version':  1.0-SNAPSHOT: :
Define value for property 'package':  org.talend.example.rest: :

Go into your newly created project folder: cd security-demo and install your test application:

mvn install

Since I like to use the Maven Jetty Plugin a lot, we need to add the jetty plugin to our pom.xml file:

<build>
    <plugins>
        <plugin>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-maven-plugin</artifactId>
            <version>9.2.3.v20140905</version>
            <configuration>
              <scanIntervalSeconds>10</scanIntervalSeconds>
              <connectors>
                <connector implementation="org.mortbay.jetty.nio.SelectChannelConnector">
                  <port>8080</port>
                  <maxIdleTime>60000</maxIdleTime>
                </connector>
              </connectors>
            </configuration>
          </plugin>
          . . .
    </plugins>
</build>

Now we can start our Demo Application with the following command:

mvn jetty:run

If we call the following URL in a browser we should get a message back from our demo application:

http://localhost:8080/hello/echo/world

Enabling Authentication for the REST Service

Currently only SAML bearer token are supported when using HTTP header to exchange the SAML token. To enable SAML Authentication Support for the REST Service we need to add the following Maven dependency to our Service project:

<dependency>
  <groupId>org.apache.cxf</groupId>
  <artifactId>cxf-rt-rs-security-xml</artifactId>
  <version>2.7.12</version>
</dependency>

In this blog post I'm going to use the HTTP Authorization Header for transmitting the SAML token from the client to the REST Service. Therefore I must update the Spring Service configuration by adding the SamlHeaderInHandler as well as the ws-security.signature.properties to validate the SAML issuer signature:

<jaxrs:server id="services" address="/">
    <jaxrs:serviceBeans>
        <bean class="org.talend.example.rest.HelloWorld" />
    </jaxrs:serviceBeans>
    <jaxrs:providers>
        <bean class="org.codehaus.jackson.jaxrs.JacksonJsonProvider" />
        <bean id="samlHandler" class="org.apache.cxf.rs.security.saml.SamlHeaderInHandler"/>
    </jaxrs:providers>
    <jaxrs:properties>
        <entry key="ws-security.signature.properties" value="alice.properties" />
    </jaxrs:properties>
</jaxrs:server>

The alice.properties need to be placed in src/main/resource folder and should look like this:

org.apache.ws.security.crypto.provider=org.apache.ws.security.components.crypto.Merlin
org.apache.ws.security.crypto.merlin.keystore.type=jks
org.apache.ws.security.crypto.merlin.keystore.password=password
org.apache.ws.security.crypto.merlin.keystore.alias=alice
org.apache.ws.security.crypto.merlin.keystore.private.password=password
org.apache.ws.security.crypto.merlin.keystore.file=alice.jks

And of course you must also place your keystore alice.jks into the src/main/resource folder.

Enabling Authorization for the REST Service

To enable XACML authorization you need to add the XACML PEP interceptor to the service provider:

<jaxrs:server id="services" address="/">
    <jaxrs:serviceBeans>
        <bean id="helloWorldService" class="org.talend.example.rest.HelloWorld" />
    </jaxrs:serviceBeans>
    <jaxrs:providers>
        <bean class="org.codehaus.jackson.jaxrs.JacksonJsonProvider" />
        <bean id="samlHandler" class="org.apache.cxf.rs.security.saml.SamlHeaderInHandler" />
    </jaxrs:providers>
    <jaxrs:properties>
        <entry key="ws-security.signature.properties" value="alice.properties" />
    </jaxrs:properties>
    <jaxrs:inInterceptors>
        <bean class="org.talend.esb.authorization.xacml.rt.pep.CXFXACMLAuthorizingInterceptor" id="XACMLInterceptor">
            <property name="pdpAddress" value="https://localhost:9001/services/pdp/authorize" />
        </bean>
    </jaxrs:inInterceptors>
</jaxrs:server> 

To make the XACML PEP Class available within your classpath you also need to add the following Maven dependency to your pom.xml:

<dependency>
    <groupId>org.talend.esb.authorization</groupId>
    <artifactId>tesb-xacml-rt</artifactId>
    <version>5.4.1</version>
</dependency> 

This dependency is available to Talend Enterprise Edition only!
If you do not want to use Talend EE you must implement the Authorization Interceptor by yourself to generate and send a valid XACML Request to the PDP.

That's it! The PEP interceptor takes the username and roles from the user from the security context which was established by the SamlHeaderInHandler.

Your REST service is now secured with SAML authentication and XACML authorization. Well of course you need to define matching XACML policies to ensure authorization and you need to setup your STS and PDP accordingly. ;-) But how you can do that will not be part of this post.

Demo Client

To call the REST Service from a REST Client and also to get a SAML Token from the STS, I designed the following JUnit Test Case:

package org.talend.example.rest;

import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

import javax.ws.rs.core.Response;

import org.apache.cxf.Bus;
import org.apache.cxf.helpers.IOUtils;
import org.apache.cxf.jaxrs.client.JAXRSClientFactoryBean;
import org.apache.cxf.jaxrs.client.WebClient;
import org.apache.cxf.rs.security.saml.SamlHeaderOutInterceptor;
import org.apache.cxf.ws.security.SecurityConstants;
import org.apache.cxf.ws.security.trust.STSClient;
import org.junit.BeforeClass;
import org.junit.Test;
import org.talend.esb.authorization.xacml.rt.pep.STSRESTOutInterceptor;

import static org.junit.Assert.assertEquals;

public class HelloWorldIT {
    private static String endpointUrl;
    private static String stsWsdl;

    @BeforeClass
    public static void beforeClass() {
        endpointUrl = System.getProperty("service.url", "http://localhost:8080");
        stsWsdl = System.getProperty("sts.wsdl", "http://localhost:8040/services/SecurityTokenService/UT?wsdl");
    }

    private WebClient createWebClient(String address, boolean selfSigned) throws Exception {
        JAXRSClientFactoryBean bean = new JAXRSClientFactoryBean();
        bean.setAddress(address);

        STSRESTOutInterceptor samlTokenProvider = new STSRESTOutInterceptor();
        samlTokenProvider.setStsClient(createSTSClient(bean.getBus()));
        bean.getOutInterceptors().add(samlTokenProvider);
        bean.getOutInterceptors().add(new SamlHeaderOutInterceptor());

        return bean.createWebClient();
    }

    private static STSClient createSTSClient(Bus bus) throws Exception {
        STSClient stsClient = new STSClient(bus);

        stsClient.setWsdlLocation(stsWsdl);
        stsClient.setServiceName("{http://docs.oasis-open.org/ws-sx/ws-trust/200512/}SecurityTokenService");
        stsClient.setEndpointName("{http://docs.oasis-open.org/ws-sx/ws-trust/200512/}UT_Port");

        Map<String, Object> properties = new HashMap<String, Object>();
        properties.put(SecurityConstants.USERNAME, "alice");
        properties.put(SecurityConstants.PASSWORD, "secret");

        stsClient.setAllowRenewingAfterExpiry(true);
        stsClient.setEnableLifetime(true);
        stsClient.setEnableAppliesTo(false);

        stsClient.setProperties(properties);
        stsClient.setTokenType("http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0");
        stsClient.setKeyType("http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer");

        stsClient.setClaimsCallbackHandler(new ClaimsCallbackHandler());

        return stsClient;
    }

    @Test
    public void testPing() throws Exception {
        WebClient client = createWebClient(endpointUrl + "/hello/echo/SierraTangoNevada", true);
        Response r = client.accept("text/plain").get();
        assertEquals(Response.Status.OK.getStatusCode(), r.getStatus());
        String value = IOUtils.toString((InputStream)r.getEntity());
        assertEquals("SierraTangoNevada", value);
    }
}
Make sure to disable the appliesTo feature when using REST services (stsClient.setEnableAppliesTo(false)). Otherwise the STS client will request a new SAML token for every distinct URL even if just the value of a single parameter of the URL has changed!

To get the STSRESTOutInterceptor on my classpath I needed to ensure that the following Maven dependency is available:

<dependency>
    <groupId>org.talend.esb.authorization</groupId>
    <artifactId>tesb-xacml-rt</artifactId>
    <version>5.4.1</version>
</dependency>

Beginning with Versino 6.0.0 the STSRESTOutInterceptor is moved from the Talend EE XACML dependency to the freely available security-common project of Talend SE.

To be code complete, here is the simple ClaimsCallbackHandler to request a role claim from the STS:

package org.talend.example.rest;

import java.io.IOException;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

import org.apache.cxf.helpers.DOMUtils;
import org.apache.cxf.ws.security.trust.claims.ClaimsCallback;

/**
 * This CallbackHandler implementation creates a Claims Element for a "role" ClaimType and
 * stores it on the ClaimsCallback object.
 */
public class ClaimsCallbackHandler implements CallbackHandler {
    
    public void handle(Callback[] callbacks)
        throws IOException, UnsupportedCallbackException {
        for (int i = 0; i < callbacks.length; i++) {
            if (callbacks[i] instanceof ClaimsCallback) {
                ClaimsCallback callback = (ClaimsCallback) callbacks[i];
                callback.setClaims(createClaims());
                
            } else {
                throw new UnsupportedCallbackException(callbacks[i], "Unrecognized Callback");
            }
        }
    }
    
    /**
     * Create a Claims Element for a "role"
     */
    private Element createClaims() {
        Document doc = DOMUtils.createDocument();
        Element claimsElement = 
            doc.createElementNS("http://docs.oasis-open.org/ws-sx/ws-trust/200512", "Claims");
        claimsElement.setAttributeNS(null, "Dialect", "http://schemas.xmlsoap.org/ws/2005/05/identity");
        Element claimType = 
            doc.createElementNS("http://schemas.xmlsoap.org/ws/2005/05/identity", "ClaimType");
        claimType.setAttributeNS(null, "Uri", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role");
        claimsElement.appendChild(claimType);
        return claimsElement;
    }
    
}

Also take a look at my following post about: Using the Talend PDP ouside of an OSGi Container.

No comments:

Post a Comment