In this document we show how to use PicketLink STS to validate SAML assertions and authenticate WS clients.
Required software: JDK 6, PicketLink 1.0.3 or superior and PicketLink Trust JBossWS 1.0.0.CR3 or superior.
Process Overview
The following picture illustrates the process of using SAML assertions to authenticate clients of EJB applications:
The client must first obtain the SAML assertion from PicketLink STS by sending a WS-Trust request to the token service. This process usually involves authentication of the client. After obtaining the SAML assertion from the STS, the client includes the assertion in the context of the WS request before invoking any operation. The client side WS handler will take care of including the assertion in the SOAP payload. Upon receiving the invocation, the server side WS handler extracts the assertion and validates it by sending a WS-Trust validate message to the STS. If the assertion is considered valid by the STS (and the proof of possession token has been verified if needed), the client is authenticated.
On JBoss, the SAML assertion validation process is handled by the SAML2STSLoginModule. It reads properties from a configurable file (specified by the configFile option) and establishes communication with the STS based on these properties. We will see how a configuration file looks like later on. If the assertion is valid, a Principal is created using the assertion subject name and if the assertion contains roles, these roles are also extracted and associated with the caller's Subject.
WS Integration Example
In this section we present a sample WS application that authenticates clients by validating their SAML assertions with PicketLink STS. The deployments for both the WS application and the STS can be found attached in this document.
WS Sample App
Our WS application consists of a simple EJB3 stateless session bean. The session interface can be seen bellow:
package webservice.test; import javax.ejb.Remote; import javax.jws.WebService; @Remote @WebService public interface WSTest { public void echo(String echo); }
And this is the implementation class:
package webservice.test; import javax.annotation.Resource; import javax.annotation.security.RolesAllowed; import javax.ejb.Stateless; import javax.jws.WebMethod; import javax.jws.WebService; import javax.xml.ws.WebServiceContext; import org.jboss.ejb3.annotation.SecurityDomain; import org.jboss.ws.annotation.EndpointConfig; @Stateless @WebService @EndpointConfig(configName = "SAML WSSecurity Endpoint") @SecurityDomain("sts") @RolesAllowed("testRole") public class WSTestBean implements WSTest { @Resource WebServiceContext wsCtx; @WebMethod public void echo(String echo) { System.out.println("WSTest: " + echo); System.out.println("Principal: " + wsCtx.getUserPrincipal()); System.out.println("Principal.getName(): " + wsCtx.getUserPrincipal().getName()); System.out.println("isUserInRole('testRole'): " + wsCtx.isUserInRole("testRole")); } }
The session requires authentication using the sts security domain and just prints whatever string the client sent and also the Principal on the server side.
This sts security domain is deployed along with the example wstest.jar file attached here but here is:
<?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> </deployment>
The policy defines two login modules: SAML2STSLoginModule and UsersRolesLoginModule. The former will be responsible for validating the assertion with the STS in order to authenticate the client and the latter is responsible for adding roles. To validate the SAML assertions the login module needs information about the STS, like its endpoint URL, service name, port name, etc. This information is supplied by the sts-config.properties file:
serviceName=PicketLinkSTS portName=PicketLinkSTSPort endpointAddress=http://localhost:8080/picketlink-sts-1.0.0/PicketLinkSTS username=JBoss password=JBoss
The last two properties specify the username and password that will be used to authenticate the JBoss server to the STS when the WS-Trust validate message is dispatched. In other words, SAML2STSLoginModule needs to authenticate to the STS when validating the SAML assertions and these properties specify the username and password that will be used for that.
The deployment wstest.jar also contains the files to map roles to the Subject. The file sts-users.properies is just an empty file as authentication is handled by PicketLink's login module. sts-roles.properties looks like this:
UserA=testRole
Notice also that the WS uses a custom endpoint (SAML WSSecurity Endpoint). This endpoint definition must be included in .../deployers/jbossws.deployer/META-INF/standard-jaxws-endpoint-config.xml:
<endpoint-config> <config-name>SAML WSSecurity Endpoint</config-name> <post-handler-chains> <javaee:handler-chain> <javaee:protocol-bindings>##SOAP11_HTTP ##SOAP11_HTTP_MTOM</javaee:protocol-bindings> <javaee:handler> <javaee:handler-name>SAML WSSecurity Handler</javaee:handler-name> <javaee:handler-class>org.picketlink.trust.jbossws.handler.SAML2Handler</javaee:handler-class> </javaee:handler> <javaee:handler> <javaee:handler-name>Recording Handler</javaee:handler-name> <javaee:handler-class>org.jboss.wsf.framework.invocation.RecordingServerHandler</javaee:handler-class> </javaee:handler> </javaee:handler-chain> </post-handler-chains> </endpoint-config>
For this endpoint to work, the library picketlink-trust-jbossws-1.0.0.CR3.jar must be included in .../deployers/jbossws.deployer/. This library is also attached here at the end of the article.
PicketLink STS
Our PicketLink STS application is a tweaked version of the picketink-sts.war file that is available in the PicketLink project downloads page. More specifically, we created a new security domain for the STS in jboss-web.xml, included an application policy for the new domain that uses the UsersRolesLoginModule to authenticate STS clients, included the users and roles properties files, and changed the required role in web.xml to STSClient.
This is the content of the STS web.xml:
<?xml version="1.0"?>
<!DOCTYPE web-app PUBLIC
-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN
http://java.sun.com/dtd/web-app_2_3.dtd>
<web-app>
<servlet>
<servlet-name>PicketLinkSTS</servlet-name>
<servlet-class>org.picketlink.identity.federation.core.wstrust.PicketLinkSTS</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>PicketLinkSTS</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
<security-constraint>
<web-resource-collection>
<web-resource-name>TokenService</web-resource-name>
<url-pattern>/*</url-pattern>
<http-method>GET</http-method>
<http-method>POST</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>STSClient</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>PicketLinkSTSRealm</realm-name>
</login-config>
<security-role>
<role-name>STSClient</role-name>
</security-role>
</web-app>
STS callers must all have the STSClient role in order to send a WS-Trust request to the STS.
The STS security domain is specified by the jboss-web.xml file:
<?xml version="1.0" encoding="UTF-8"?>
<jboss-web>
<security-domain>java:/jaas/sts-domain</security-domain>
</jboss-web>
The application policy for the sts-domain is defined in the sts-jboss-beans.xml file:
<?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-domain">
<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>
The sts-users.properties specify the username/passwords of the STS callers:
JBoss=JBoss
UserA=PassA
UserB=PassB
UserC=PassC
The sts-roles.properties specify the roles of the STS callers:
JBoss=STSClient
UserA=STSClient
UserB=STSClient
UserC=STSClient
Notice that the JBoss user represents the JBoss server during the SAML validation process. All other users are the clients of the WS sample application - they send a message to the STS to acquire a SAML assertion before calling the methods on the WS application.
Client Application
STSWSClient just acquires a SAML assertion from PicketLink STS and invokes the echo method of the WS:
package webservice.test; import java.net.URL; import org.w3c.dom.Element; import javax.xml.namespace.QName; import javax.xml.ws.BindingProvider; import javax.xml.ws.Service; import org.jboss.ws.core.StubExt; import org.picketlink.identity.federation.api.wstrust.WSTrustClient.SecurityInfo; import org.picketlink.identity.federation.api.wstrust.WSTrustClient; import org.picketlink.identity.federation.core.wstrust.WSTrustException; import org.picketlink.identity.federation.core.wstrust.plugins.saml.SAMLUtil; import org.picketlink.trust.jbossws.SAML2Constants; public class STSWSClient { private static String username = "UserA"; private static String password = "PassA"; public static void main(String[] args) throws Exception { WSTrustClient client = new WSTrustClient("PicketLinkSTS", "PicketLinkSTSPort", "http://localhost:8080/picketlink-sts-1.0.0/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); } URL wsdl = new URL("http://127.0.0.1:8080/wstest/WSTestBean?wsdl"); QName serviceName = new QName("http://test.webservice/", "WSTestBeanService"); Service service = Service.create(wsdl, serviceName); WSTest port = service.getPort(new QName("http://test.webservice/", "WSTestBeanPort"), WSTest.class); ((StubExt) port).setConfigName("SAML WSSecurity Client"); ((BindingProvider) port).getRequestContext().put(SAML2Constants.SAML2_ASSERTION_PROPERTY, assertion); port.echo("Test"); } }
As we can see, the assertion is first obtained using the WSTrustClient API. Notice the client then adds the SAML assertion in the SAML2Constants.SAML2_ASSERTION_PROPERTY ("org.picketlink.trust.saml.assertion") property in the request context before invoking the echo method.
The client also uses a custom endpoint configuration (SAML WSSecurity Client) that must be included in the client's META-INF/standard-jaxws-client-config.xml:
<?xml version="1.0" encoding="UTF-8"?> <jaxws-config xmlns="urn:jboss:jaxws-config:2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:javaee="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="urn:jboss:jaxws-config:2.0 schema/jaxws-config_2_0.xsd"> ... <client-config> <config-name>SAML WSSecurity Client</config-name> <post-handler-chains> <javaee:handler-chain> <javaee:protocol-bindings>##SOAP11_HTTP ##SOAP11_HTTP_MTOM</javaee:protocol-bindings> <javaee:handler> <javaee:handler-name>SAML WSSecurity Client Handler</javaee:handler-name> <javaee:handler-class>org.picketlink.trust.jbossws.handler.SAML2Handler</javaee:handler-class> </javaee:handler> </javaee:handler-chain> </post-handler-chains> </client-config> </jaxws-config>
Deploying and Running the EJB3 Sample Application on JBoss AS5
In order to get the sample application running you must first install the PicketLink jar files on JBoss. This is accomplished by copying picketlink-fed-2.0.0-SNAPSHOT.jar and picketlink-bindings-jboss-2.0.0-SNAPSHOT.jar (both attached in this document) files to the $JBOSS_HOME/server/<config>/lib folder. After installing the required PicketLink libs you must copy the wstest.jar and picketlink-sts-1.0.0.war to $JBOSS_HOME/server/<config>/deploy.
After copying the required PicketLink jars and deploying the sample application and the STS war, start your JBoss instance. If everything is ok, you should see something like the following in the log:
16:36:30,062 INFO [JBossASKernel] Created KernelDeployment for: wstest.jar 16:36:30,069 INFO [JBossASKernel] installing bean: jboss.j2ee:jar=wstest.jar,name=WSTestBean,service=EJB3 16:36:30,069 INFO [JBossASKernel] with dependencies: 16:36:30,069 INFO [JBossASKernel] and demands: 16:36:30,069 INFO [JBossASKernel] jboss.ejb:service=EJBTimerService 16:36:30,070 INFO [JBossASKernel] and supplies: 16:36:30,070 INFO [JBossASKernel] Class:webservice.test.WSTest 16:36:30,070 INFO [JBossASKernel] jndi:WSTestBean/remote 16:36:30,070 INFO [JBossASKernel] jndi:WSTestBean/remote-webservice.test.WSTest 16:36:30,070 INFO [JBossASKernel] Added bean(jboss.j2ee:jar=wstest.jar,name=WSTestBean,service=EJB3) to KernelDeployment of: wstest.jar 16:36:30,214 INFO [SessionSpecContainer] Starting jboss.j2ee:jar=wstest.jar,name=WSTestBean,service=EJB3 16:36:30,227 INFO [EJBContainer] STARTED EJB: webservice.test.WSTestBean ejbName: WSTestBean 16:36:30,354 INFO [JndiSessionRegistrarBase] Binding the following Entries in Global JNDI: WSTestBean/remote - EJB3.x Default Remote Business Interface WSTestBean/remote-webservice.test.WSTest - EJB3.x Remote Business Interface 16:36:30,687 INFO [DefaultEndpointRegistry] register: jboss.ws:context=wstest,endpoint=WSTestBean 16:36:31,924 INFO [WSDLFilePublisher] WSDL published to: file:/opt/jboss-eap-5.0/jboss-as/server/wstest/data/wsdl/wstest.jar/WSTestBeanService7960307186338997650.wsdl 16:36:32,048 INFO [TomcatDeployment] deploy, ctxPath=/wstest 16:36:32,102 WARNING [config] Unable to process deployment descriptor for context '/wstest' 16:36:32,136 INFO [config] Initializing Mojarra (1.2_13-b01-FCS) for context '/wstest' 16:36:35,856 INFO [DefaultEndpointRegistry] register: jboss.ws:context=picketlink-sts-1.0.0,endpoint=PicketLinkSTS 16:36:35,936 INFO [TomcatDeployment] deploy, ctxPath=/picketlink-sts-1.0.0 16:36:36,120 INFO [WSDLFilePublisher] WSDL published to: file:/opt/jboss-eap-5.0/jboss-as/server/wstest/data/wsdl/picketlink-sts-1.0.0.war/PicketLinkSTS.wsdl
To run the client application some JBoss' libraries must be included in the classpath:
java -Djava.endorsed.dirs=/opt/jboss-eap-5.0/jboss-as/lib/endorsed -classpath /opt/jboss-eap-5.0/jboss-as/client/jbossall-client.jar:/opt/jboss-eap-5.0/jboss-as/client/jbossws-native-core.jar:/opt/jboss-eap-5.0/jboss-as/client/jbossws-spi.jar:/opt/jboss-eap-5.0/jboss-as/client/jbossws-common.jar:/opt/jboss-eap-5.0/jboss-as/client/jbossws-native-jaxrpc.jar:/opt/jboss-eap-5.0/jboss-as/client/mail.jar:../lib/picketlink-bindings-jboss-2.0.0-SNAPSHOT.jar:../lib/picketlink-fed-2.0.0-SNAPSHOT.jar:../lib/picketlink-trust-jbossws-1.0.0.CR3.jar:/opt/jboss-eap-5.0/jboss-as/client/jboss-xml-binding.jar:/opt/jboss-eap-5.0/jboss-as/client/jaxb-impl.jar:/opt/jboss-eap-5.0/jboss-as/client/wsdl4j.jar:../lib/wstest.jar:. webservice.test.STSWSClient
If everything has been configured and deployed correctly, you should see this output in the client's side:
Invoking token service to get SAML assertion for UserA SAML assertion for UserA successfully obtained!
And this in the server's side:
15:56:19,040 INFO [PicketLinkSTS] Loading STS configuration 15:56:19,145 INFO [PicketLinkSTS] picketlink-sts.xml configuration file loaded 15:56:21,648 INFO [STDOUT] WSTest: Test 15:56:21,658 INFO [STDOUT] Principal: org.picketlink.identity.federation.bindings.jboss.subject.PicketLinkPrincipal@4e39dd5 15:56:21,658 INFO [STDOUT] Principal.getName(): UserA 15:56:21,669 INFO [STDOUT] isUserInRole('testRole'): true
As we can see, the echo method was invoked and the Principal is an instance of PicketLinkPrincipal (meaning the authentication was successful). Invocation of the web service was successful and also WebServiceContext.isUserInRole("testRole") returns true meaning authorization was also successful.
Special thanks to David Boeren for helping out with the WS client part.
Comments