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