In this article, we will explore enabling SAML authentication for POJO based Web Services using JBoss Web Services (JBossWS). This also satisfies the SAML Token Profile of the Oasis Web Services Security Specification.
Requisities
- JBossWS in JBoss AS.
- PicketLink 2.0 distribution ( )
POJO Web Services
Lets write an interface.
package org.picketlink.test.trust.ws; import javax.ejb.Remote; import javax.jws.WebService; @Remote @WebService public interface WSTest{ public String echo(String echo); public String echoUnchecked(String echo); }
Lets write a POJO implementing this interface.
package org.picketlink.test.trust.ws; import javax.jws.HandlerChain; import javax.jws.WebMethod; import javax.jws.WebService; import javax.jws.soap.SOAPBinding; @WebService @SOAPBinding(style = SOAPBinding.Style.RPC) @HandlerChain(file="authorize-handlers.xml") public class POJOBean { @WebMethod public String echo(String echo) { return echo; } @WebMethod public String echoUnchecked(String echo) { return echo; } }
Notice that we have indicated the use of an xml file to describe the handlers. The handler file is authorize-handlers.xml
<?xml version="1.0" encoding="UTF-8"?> <handler-chains xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee javaee_web_services_1_2.xsd"> <handler-chain> <handler> <handler-name>WSAuthorizationHandler</handler-name> <handler-class>org.picketlink.trust.jbossws.handler.WSAuthorizationHandler</handler-class> </handler> <handler> <handler-name>WSAuthenticationHandler</handler-name> <handler-class>org.picketlink.trust.jbossws.handler.WSAuthenticationHandler</handler-class> </handler> <handler> <handler-name>SAML2Handler</handler-name> <handler-class>org.picketlink.trust.jbossws.handler.SAML2Handler</handler-class> </handler> </handler-chain> </handler-chains>
The order of the handlers is very important. They are defined in the reverse order of execution as per the JAX-WS Specification.
See section 9.3.2 of JAXWS 2.2 spec:
"For outbound messages handler processing starts with the first handler in the chain and proceeds in the same order as the handler chain. For inbound messages the order of processing is reversed: processing starts with the last handler in the chain and proceeds in the reverse order of the handler chain. E.g., consider a handler chain that consists of six handlers H1 . . .H6 in that order: for outbound messages handler H1 would be
invoked first followed by H2, H3, . . . , and finally handler H6; for inbound messages H6 would be invoked first followed by H5, H4, . . . , and finally H1."
Since POJO web services are packaged as web archives (WAR) files, we need a web.xml also
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <servlet> <display-name>POJO Web Service</display-name> <servlet-name>POJOBeanService</servlet-name> <servlet-class>org.picketlink.test.trust.ws.POJOBean</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>POJOBeanService</servlet-name> <url-pattern>/POJOBeanService</url-pattern> </servlet-mapping> </web-app>
Note, we are NOT adding any security-constraint elements in the web.xml
Securing the POJO Web Services
We need to introduce a jboss-web.xml in the WEB-INF directory of the web archive.
<jboss-web> <security-domain>sts</security-domain> </jboss-web>
We also need the JBossWS deployment descriptor for Web Services Security, jboss-wsse.xml in the WEB-INF directory.
<jboss-ws-security xmlns="http://www.jboss.com/ws-security/config" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.jboss.com/ws-security/config http://www.jboss.com/ws-security/schema/jboss-ws-security_1_0.xsd"> <port name="POJOBeanPort"> <operation name="{http://ws.trust.test.picketlink.org/}echoUnchecked"> <config> <authorize> <unchecked/> </authorize> </config> </operation> <operation name="{http://ws.trust.test.picketlink.org/}echo"> <config> <authorize> <role>JBossAdmin</role> </authorize> </config> </operation> </port> </jboss-ws-security>
As you can see, we have defined the access control rules for the two operations for a port, POJOBeanPort.
The operation, echoUnchecked gives unlimited access whereas the operation, echo needs a role "JBossAdmin" in the caller.
Package the Web Archive
You will need to package the Web Archive with the web.xml, jboss-web.xml and jboss-wsse.xml in the WEB-INF directory. Also package the compiled classes for WSTest interface and POJOBean in the classes directory.
Client/ Test Classes
We are going to write a test case using JUnit.
package org.picketlink.test.trust.tests; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import java.net.URL; import java.util.List; import javax.xml.namespace.QName; import javax.xml.ws.BindingProvider; import javax.xml.ws.Service; import javax.xml.ws.handler.Handler; import javax.xml.ws.soap.SOAPFaultException; import org.junit.Test; import org.picketlink.test.trust.ws.WSTest; import org.picketlink.trust.jbossws.SAML2Constants; import org.picketlink.trust.jbossws.handler.SAML2Handler; import org.w3c.dom.Element; /** * A Simple WS Test for POJO WS Authorization using PicketLink */public class POJOWSAuthorizationTestCase extends TrustTestsBase { @SuppressWarnings("rawtypes") @Test public void testWSInteraction() throws Exception { Element assertion = getAssertionFromSTS("UserA", "PassA"); // Step 2: Stuff the Assertion on the SOAP message context and add the SAML2Handler to client side handlers URL wsdl = new URL("http://localhost:8080/pojo-test/POJOBeanService?wsdl"); QName serviceName = new QName("http://ws.trust.test.picketlink.org/", "POJOBeanService"); Service service = Service.create(wsdl, serviceName); WSTest port = service.getPort(new QName("http://ws.trust.test.picketlink.org/", "POJOBeanPort"), WSTest.class); BindingProvider bp = (BindingProvider)port; bp.getRequestContext().put(SAML2Constants.SAML2_ASSERTION_PROPERTY, assertion); List<Handler> handlers = bp.getBinding().getHandlerChain(); handlers.add(new SAML2Handler()); bp.getBinding().setHandlerChain(handlers); //Step 3: Access the WS. Exceptions will be thrown anyway. assertEquals( "Test", port.echo("Test")); } @SuppressWarnings("rawtypes") @Test public void testWSAccessDeniedInteraction() throws Exception { Element assertion = getAssertionFromSTS("UserB", "PassB"); // Step 2: Stuff the Assertion on the SOAP message context and add the SAML2Handler to client side handlers URL wsdl = new URL("http://localhost:8080/pojo-test/POJOBeanService?wsdl"); QName serviceName = new QName("http://ws.trust.test.picketlink.org/", "POJOBeanService"); Service service = Service.create(wsdl, serviceName); WSTest port = service.getPort(new QName("http://ws.trust.test.picketlink.org/", "POJOBeanPort"), WSTest.class); BindingProvider bp = (BindingProvider)port; bp.getRequestContext().put(SAML2Constants.SAML2_ASSERTION_PROPERTY, assertion); List<Handler> handlers = bp.getBinding().getHandlerChain(); handlers.add(new SAML2Handler()); bp.getBinding().setHandlerChain(handlers); try { port.echo("Test"); fail( "Should have thrown exception"); } catch( Exception e) { if(e instanceof SOAPFaultException) { //pass } else fail( "Wrong Exception:"+e); } } @SuppressWarnings("rawtypes") @Test public void testWSUncheckedInteraction() throws Exception { Element assertion = getAssertionFromSTS("UserB", "PassB"); // Step 2: Stuff the Assertion on the SOAP message context and add the SAML2Handler to client side handlers URL wsdl = new URL("http://localhost:8080/pojo-test/POJOBeanService?wsdl"); QName serviceName = new QName("http://ws.trust.test.picketlink.org/", "POJOBeanService"); Service service = Service.create(wsdl, serviceName); WSTest port = service.getPort(new QName("http://ws.trust.test.picketlink.org/", "POJOBeanPort"), WSTest.class); BindingProvider bp = (BindingProvider)port; bp.getRequestContext().put(SAML2Constants.SAML2_ASSERTION_PROPERTY, assertion); List<Handler> handlers = bp.getBinding().getHandlerChain(); handlers.add(new SAML2Handler()); bp.getBinding().setHandlerChain(handlers); //Step 3: Access the WS. Exceptions will be thrown anyway. assertEquals( "Test", port.echoUnchecked("Test")); } }
package org.picketlink.test.trust.tests; import org.picketlink.identity.federation.api.wstrust.WSTrustClient; import org.picketlink.identity.federation.api.wstrust.WSTrustClient.SecurityInfo; import org.picketlink.identity.federation.core.wstrust.WSTrustException; import org.picketlink.identity.federation.core.wstrust.plugins.saml.SAMLUtil; import org.w3c.dom.Element; /** * Base class for the PicketLink trust tests */ public class TrustTestsBase { /** * Method gets a SAML assertion from the PicketLink STS * @param username username to send to STS * @param password password to send to STS * @return * @throws Exception */ protected Element getAssertionFromSTS(String username, String password) throws Exception { // Step 1: Get a SAML2 Assertion Token from the STS WSTrustClient client = new WSTrustClient("PicketLinkSTS", "PicketLinkSTSPort", "http://localhost:8080/picketlink-sts/PicketLinkSTS", new SecurityInfo(username, password)); Element assertion = null; try { System.out.println("Invoking token service to get SAML assertion for " + username); assertion = client.issueToken(SAMLUtil.SAML2_TOKEN_TYPE); System.out.println("SAML assertion for " + username + " successfully obtained!"); } catch (WSTrustException wse) { System.out.println("Unable to issue assertion: " + wse.getMessage()); wse.printStackTrace(); System.exit(1); } return assertion; } }
The test case calls the PicketLink STS to obtain a SAML Assertion and then it is sent on the WS call to the POJO bean. We add the SAML2Handler on the client side also. The SAML2Handler will pick up the SAML assertion and then send it as part of the WS request.
PicketLink STS
The STS running on JBoss AS can be configured with the following security domain definition. (sts-jboss-beans.xml added to the deploy directory)
<?xml version="1.0" encoding="UTF-8"?> <deployment xmlns="urn:jboss:bean-deployer:2.0"> <!-- ejb3 test application-policy definition --> <application-policy xmlns="urn:jboss:security-beans:1.0" name="sts"> <authentication> <login-module code="org.picketlink.identity.federation.bindings.jboss.auth.SAML2STSLoginModule" flag="required"> <module-option name="configFile">sts-config.properties</module-option> <module-option name="password-stacking">useFirstPass</module-option> </login-module> <login-module code="org.jboss.security.auth.spi.UsersRolesLoginModule" flag="required"> <module-option name="usersProperties">sts-users.properties</module-option> <module-option name="rolesProperties">sts-roles.properties</module-option> <module-option name="password-stacking">useFirstPass</module-option> </login-module> </authentication> </application-policy> <!-- ejb3 test application-policy definition --> <application-policy xmlns="urn:jboss:security-beans:1.0" name="jmx-console"> <authentication> <login-module code="org.jboss.security.auth.spi.UsersRolesLoginModule" flag="required"> <module-option name="usersProperties">sts-users.properties</module-option> <module-option name="rolesProperties">sts-roles.properties</module-option> </login-module> </authentication> </application-policy> </deployment>
In the "sts" security domain, we have defined two property files, sts-users.properties and sts-roles.properties which can be dropped into the conf directory of JBoss AS and will look as follows:
sts-users.properties
JBoss=JBoss UserA=PassA UserB=PassB UserC=PassC admin=admin
sts-roles.properties
JBoss=STSClient UserA=STSClient,testRole,JBossAdmin UserB=STSClient UserC=STSClient admin=JBossAdmin
References
Comments