Survey
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
Ping Java SDK and Web Services (WS-Trust) In an effort to get my head around Java Web Services I have thrown together a quick Client/Service scenario to learn how everything works. I am not the most advanced Java developer and I am continually confused by all the different frameworks and interfaces that Java provides so I wanted to build a default sandbox that I could play with to test certain web services scenarios. I have a few scenarios here that all build from a common base. I have been using NetBeans for this but you can pretty much insert your favourite Java IDE (ie Eclipse) as you wish – you may need to google some of the IDE things like configuring handlers etc. • Step One: Basic Web Service provider (WS) and Web Service Client (WSC) • Step Two: Enabling WS-Security on the WS (require SAML token) • Step Three: Enabling WS-Security on the WSC (swap username token for SAML token) • Step Four: Receiving a local SAML token for the WS and returning the subject to the web service This may help as a learning tool or as the basics for a POC, or at least to save time from jumping through multiple examples and sample code to produce a simple WS-Trust scenario. Pre-requisites • • • • Java IDE with EE capable web server (ie Netbeans with Glassfish) PingFederate with STS enabled Ping Java SDK Ping WSS Username Token Translator Disclaimer This sample was farmed from various code snippets found all over the Internet, SDK samples and via my own keyboard. This is not guaranteed to work or be safe, secure or well-performing by any means. Good for a POC or to learn the concepts but nothing else. PingFederate Configuration For all scenarios, the following PingFederate configuration is used: Ping server is listening on https://localhost:9031. https://localhost:9031/idp/sts.wst (and /sp/sts.wst) So the STS endpoint is I just have a self-signed SSL certificate so you will notice the following line in the code: stsClientConfiguration.setIgnoreSSLTrustErrors(true); code: stsClientConfiguration.setIgnoreSSLTrustErrors(true); IDP Side (to handle the authentication of the Web Services client): Token Processor Username Token Processor 1.1 configured to verify a credential set (either named users, LDAP etc). For this sample I used the “User Table with Password” option to manually defined users SP Connection WS-Trust connection configured as the screenshot below: SP Side (to service the WS (validate token + re-issue local token)) Token Generator SAML 2.0 Token Generator 1.1 used to issue a local SAML token for the Web Service and to validate an existing SAML token. Configuration for the token generator is below: IDP Connection Used to interface with the Web Service. Defines the attribute contract, signing cert, issuer etc of the service. Configuration used is included below: Step One: Basic Web Service provider (WS) and Web Service Client (WSC) Launch the NetBeans IDE and create a new Java Web Application project to create our sample Web Service. Give the project a name… I am using the built-in Glassfish server. Shouldn’t need to modify anything on this screen so you can hit Finish. Now we have a basic Web Application. We need to create the Web Service to sit in here. Right-click on the name of the project and choose New > Web Service… | Give the service a name and a package name. Also check the box to implement as a “Stateless Session Bean” Now we have a basic web service that will say “Hello” to a name you provide. To test the service: Right-click the Project and select “Deploy” to deploy it to the Glassfish web server. Expand the “Web Services” folder under the project, right-click on the service and select “Test Web Service” to verify the service is correctly running. Once you have created the service, we will now create a web service client to communicate with this web service. In the NetBeans IDE, create a new Java Application by choosing File > New Project: Name the project and click Finish to create a basic Java application. To create a reference to the Web Service we created, in the project explorer, right-click on the application project name and select: New > Web Service Client… Choose to specify the WSDL from the Web Service project we created earlier and click Finish to create the reference. Now we will add this reference into the Java code to call the web service from our Java app. Open the Java application source file in the editor. On the Project explorer, expand the levels under the “Web Service References” until you see the web service method (red dot) and drag that into your code (under the closing brace of the “main method”) Modify the main method to call this new method: Right-click the project and choose “Run” to execute the client. You should see the results of the web service call in the console: Success. We now have a working web service and web service client configuration. Step Two: Enabling WS-Security on the WS to require SAML security token The next step is to protect the existing web service. The requirements now are that no one can call this web service without providing a valid SAML security token. To protect the web service, we are going to use the Ping Java SDK to integrate with the Ping STS. We are going to create a SOAP Handler to handle the SAML validation without having to change the core web service code. Because we are using the Ping SDK, we need to add the required libraries to our project. In the project explorer, right-click “Libraries” and select “Add JAR/Folder”. Browse to the location where you expanded the Ping Java SDK and select all the jars in the “lib” folder. In the Web Service project (SampleWS). create a new Java class (we call it SampleWSHandler): Paste the following code into this Java file: [You may need to modify the package, class name and STS endpoint appropriately] package com.pingidentity.pmeyer.ws; import import import import import import import import import import import import import import java.net.MalformedURLException; java.util.Set; javax.xml.namespace.QName; javax.xml.soap.SOAPBody; javax.xml.soap.SOAPException; javax.xml.soap.SOAPFault; javax.xml.soap.SOAPHeader; javax.xml.soap.SOAPMessage; javax.xml.ws.handler.MessageContext; javax.xml.ws.handler.soap.SOAPHandler; javax.xml.ws.handler.soap.SOAPMessageContext; javax.xml.ws.soap.SOAPFaultException; org.w3c.dom.Element; org.w3c.dom.NodeList; /** * * @author pmeyer */ public class SampleWSHandler implements SOAPHandler<SOAPMessageContext> { private static String STS_ENDPOINT_URL = "https://localhost:9031/sp/sts.wst"; @Override public boolean handleMessage(SOAPMessageContext messageContext) { Boolean isRequest = (Boolean) messageContext.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY); //for response message only, true for outbound messages, false for inbound if (!isRequest) { try { SOAPMessage soapMsg = messageContext.getMessage(); SOAPHeader soapHeader = soapMsg.getSOAPHeader(); NodeList secHeaders = soapHeader .getElementsByTagNameNS( "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wsswssecurity-secext-1.0.xsd", "Security"); if (secHeaders.getLength() == 0) { generateSOAPErrMessage(soapMsg, "No Security Header"); } Element securityHeader = (Element) secHeaders.item(0); STSClientConfiguration stsClientConfiguration = new STSClientConfiguration(); stsClientConfiguration.setStsEndpoint(STS_ENDPOINT_URL); stsClientConfiguration.setIgnoreSSLTrustErrors(true); STSClient client; try { client = new STSClient(stsClientConfiguration); client = new STSClient(stsClientConfiguration); } catch (MalformedURLException e) { throw new RuntimeException(e); } SamlToken token; try { token = client.extractTokenFromSecurityHeader(securityHeader); } catch (SecurityTokenException e) { throw new RuntimeException(e); } if (token == null) { generateSOAPErrMessage(soapMsg, "No security token found"); } boolean valid = false; try { valid = client.validateToken(token); } catch (STSClientException e) { generateSOAPErrMessage(soapMsg, e.getMessage()); } if (!valid) { generateSOAPErrMessage(soapMsg, "Security token invalid"); } else { // We have a valid token... System.out.println("WSP: VALID..."); } } catch (SOAPException e) { System.err.println(e); } } } return true; @Override public boolean handleFault(SOAPMessageContext context) { return true; } @Override public void close(MessageContext context) { } @Override public Set<QName> getHeaders() { return null; } } private void generateSOAPErrMessage(SOAPMessage msg, String reason) { try { SOAPBody soapBody = msg.getSOAPPart().getEnvelope().getBody(); SOAPFault soapFault = soapBody.addFault(); soapFault.setFaultString(reason); throw new SOAPFaultException(soapFault); } catch (SOAPException e) { } } Next we need to attach this handler to the existing web service code. In the project explorer, right-click the “SampleWS” service under the “Web Services” folder in the project and select “Configure Handlers…”. Click the Add button and browse to the handler file we just created (SimpleWSHandler.java). Click okay to apply that change. To test the changes, re-deploy the web service by right-clicking on the Project name (SampleWS) and selecting “Deploy”. Right-click your Client project and choose “Run”. You should get an error indicating that no security header was received. Pretty cool that we only added the handler to the project. Minimal changes to the core web service code were needed to secure it. [TODO: Modify the WSDL to advertise that we are expecting a SAML security token] Step Three: Enabling WS-Security on the WSC (swap username token for SAML token) Now we will follow a similar process on the client side. We want to authenticate a user via the STS and present a SAML 2.0 assertion in the SOAP headers for the Web Service to consume. As with the Web Service provider, in the Client project, right-click the “Libraries” folder and add the JARs from the Java SDK. We now can add a handler on the Client side. Right-click the project and create a new Java Class… In this java file, copy the following code. You can modify the contents appropriately: package sampleclient; import import import import import import import import import import import import com.pingidentity.sts.clientapi.STSClient; com.pingidentity.sts.clientapi.STSClientConfiguration; com.pingidentity.sts.clientapi.STSClientException; com.pingidentity.sts.clientapi.utils.StringUtils; java.net.MalformedURLException; java.util.Set; java.util.TreeSet; javax.annotation.Resource; javax.xml.namespace.QName; javax.xml.soap.*; javax.xml.ws.handler.MessageContext; javax.xml.ws.handler.soap.SOAPHandler; import javax.xml.ws.handler.soap.SOAPMessageContext; import org.w3c.dom.Element; public class SampleClientHandler implements SOAPHandler<SOAPMessageContext> { private private private private static static static static String String String String WSS_USERNAME_TOKEN_USERNAME = "user123"; WSS_USERNAME_TOKEN_PASSWORD = "User123"; STS_ENDPOINT_URL = "https://localhost:9031/idp/sts.wst"; APPLIES_TO = "http://localhost"; public SampleClientHandler() { } @Override public boolean handleMessage(SOAPMessageContext context) { Boolean outboundProperty = (Boolean) context.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY); if (outboundProperty.booleanValue()) { // We are a Web Services Client. We need to get a SAML token to send through to our service. // First of all, request a security token from the STS: STSClientConfiguration stsClientConfiguration = new STSClientConfiguration(); stsClientConfiguration.setStsEndpoint(STS_ENDPOINT_URL); stsClientConfiguration.setAppliesTo(APPLIES_TO); stsClientConfiguration.setIgnoreSSLTrustErrors(true); STSClient client; // instantiate the STS client try { client = new STSClient(stsClientConfiguration); } catch (MalformedURLException e) { throw new RuntimeException(e); } // Send in a Username token and receive the issued SAML token Element token; try { token = client.issueToken(WSS_USERNAME_TOKEN_USERNAME, WSS_USERNAME_TOKEN_PASSWORD); } catch (STSClientException e) { // deal with the exception throw new RuntimeException(e); } // We now have a SAML token to include in the Web Services Request System.out.println("WSC: RST swapped for SAML token"); StringUtils ppUtil = new StringUtils(); ppUtil.prettyPrint(token); // Insert the SAML token into the SOAP Headers... try { SOAPEnvelope envelope = context.getMessage().getSOAPPart().getEnvelope(); SOAPFactory factory = SOAPFactory.newInstance(); String prefix = "wsse"; String uri = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wsswssecurity-secext-1.0.xsd"; SOAPElement securityElem = factory.createElement("Security", prefix, uri); SOAPElement tokenElement = factory.createElement(token); securityElem.addChildElement(tokenElement); SOAPHeader header = envelope.addHeader(); header.addChildElement(securityElem); } catch (Exception e) { e.printStackTrace(); } } else { // inbound } } return true; @Override public Set<QName> getHeaders() { return new TreeSet(); } @Override public boolean handleFault(SOAPMessageContext context) { public boolean handleFault(SOAPMessageContext context) { return false; } } @Override public void close(MessageContext context) { // } Again we must attach this handler to the client code by performing the following steps. In the project explorer, expand the “Web Service References” folder, right-click on the web service (SampleWS) and choose “Configure Handlers…” Click the Add button and browse to the Handler we just created. Click OK to apply the handler. Right-click the Client Project and select Run. Assuming the username/password you are passing to the wss username token processor are correct you should be able to execute the web service call successfully: Now we have a web services client and server talking to each other and being secured via SAML/WS-Security. Useful tests at this point are to: • Change the credentials and see what a failed authentication looks like. • Change the certificate used to sign the SAML token on the IDP side to see what an invalid assertion looks like. Step Four: Issuing and consuming a local SAML token on the Web Service Now that the service is protected we now want to personalize the web service. So now we want to grab a local SAML token and say hello to the subject of the authentication. In the web service project, update the Handler using the code below. Bold is changed lines: package com.pingidentity.pmeyer.ws; import javax.xml.soap.*; import import import import import javax.xml.soap.*; javax.xml.ws.handler.MessageContext; javax.xml.ws.handler.soap.SOAPHandler; javax.xml.ws.handler.soap.SOAPMessageContext; javax.xml.ws.soap.SOAPFaultException; import java.net.MalformedURLException; import java.util.Set; import javax.xml.namespace.QName; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import import import import import import import com.pingidentity.sts.clientapi.STSClient; com.pingidentity.sts.clientapi.STSClientConfiguration; com.pingidentity.sts.clientapi.STSClientException; com.pingidentity.sts.clientapi.SecurityTokenException; com.pingidentity.sts.clientapi.tokens.saml.SamlToken; com.pingidentity.sts.clientapi.tokens.saml.Saml20Token; com.pingidentity.sts.clientapi.utils.StringUtils; /** * * @author pmeyer */ public class SampleWSHandler implements SOAPHandler<SOAPMessageContext> { private static String STS_ENDPOINT_URL = "https://localhost:9031/sp/sts.wst"; @Override public boolean handleMessage(SOAPMessageContext messageContext) { System.out.println("In WSP getMessage..."); Boolean isRequest = (Boolean) messageContext.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY); //for response message only, true for outbound messages, false for inbound if (!isRequest) { try { SOAPMessage soapMsg = messageContext.getMessage(); SOAPHeader soapHeader = soapMsg.getSOAPHeader(); NodeList secHeaders = soapHeader .getElementsByTagNameNS( "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wsswssecurity-secext-1.0.xsd", "Security"); if (secHeaders.getLength() == 0) { generateSOAPErrMessage(soapMsg, "No Security Header"); } Element securityHeader = (Element) secHeaders.item(0); STSClientConfiguration stsClientConfiguration = new STSClientConfiguration(); stsClientConfiguration.setStsEndpoint(STS_ENDPOINT_URL); stsClientConfiguration.setIgnoreSSLTrustErrors(true); STSClient client; try { client = new STSClient(stsClientConfiguration); } catch (MalformedURLException e) { throw new RuntimeException(e); } SamlToken token; try { token = client.extractTokenFromSecurityHeader(securityHeader); } catch (SecurityTokenException e) { throw new RuntimeException(e); } if (token == null) { generateSOAPErrMessage(soapMsg, "No security token found"); } boolean valid = false; try { valid = client.validateToken(token); } catch (STSClientException e) { generateSOAPErrMessage(soapMsg, e.getMessage()); } } if (!valid) { generateSOAPErrMessage(soapMsg, "Security token invalid"); } else { // We have a valid token... did we swap for local token? System.out.println("WSP: VALID..."); try { Element localTokenXML = client.issueToken(token); SamlToken localToken = new Saml20Token(localTokenXML); String nameID = localToken.getNameIdentifier(); messageContext.put("subject", nameID); messageContext.setScope("subject", MessageContext.Scope.APPLICATION); System.out.println("WSP: SAML Identity is:"); System.out.println(nameID); } catch (Exception stsE) { // probably an invalid local SAML token returned System.out.println(stsE.getMessage()); } } } catch (SOAPException e) { System.err.println(e); } } } return true; @Override public boolean handleFault(SOAPMessageContext context) { return true; } @Override public void close(MessageContext context) { } @Override public Set<QName> getHeaders() { return null; } } private void generateSOAPErrMessage(SOAPMessage msg, String reason) { try { SOAPBody soapBody = msg.getSOAPPart().getEnvelope().getBody(); SOAPFault soapFault = soapBody.addFault(); soapFault.setFaultString(reason); throw new SOAPFaultException(soapFault); } catch (SOAPException e) { } } With these extra lines we are requesting a new local SAML token and parsing the subject out of there. (Note: there is no reason you can’t use the subject from the initial token received, I was just playing with the issuance of a local token at this point) We are also passing along the subject of the SAML token in the message context. So to retrieve that from the web services code we need to modify the web service as such: package com.pingidentity.pmeyer.ws; import import import import import import import import javax.annotation.Resource; javax.jws.WebService; javax.jws.WebMethod; javax.jws.WebParam; javax.ejb.Stateless; javax.jws.HandlerChain; javax.xml.ws.WebServiceContext; javax.xml.ws.handler.MessageContext; /** * * @author pmeyer */ */ @WebService(serviceName = "SampleWS") @Stateless() @HandlerChain(file = "SampleWS_handler.xml") public class SampleWS { @Resource private WebServiceContext context; @WebMethod(operationName = "hello") public String hello(@WebParam(name = "name") String txt) { MessageContext msgContext = context.getMessageContext(); String thisUser = "[Unknown]"; if (msgContext.containsKey("subject")) { thisUser = msgContext.get("subject").toString(); } } } return "Hello " + thisUser + " !"; Now when you run the client you should see the subject of the security token being welcomed: Things to try at this point are to add attributes to the assertion and pass them through to the web service. Along the same lines, to pass values from the Web Services client through to the Handler, you can modify the Web Services client code as following to pass the username and password: Web Services client code: package sampleclient; import javax.xml.ws.BindingProvider; /** * * @author pmeyer */ public class SampleClient { /** * @param args the command line arguments */ public static void main(String[] args) { System.out.println(hello("Oscar")); } private static String hello(java.lang.String name) { com.pingidentity.pmeyer.ws.SampleWS_Service service = new com.pingidentity.pmeyer.ws.SampleWS_Service(); com.pingidentity.pmeyer.ws.SampleWS port = service.getSampleWSPort(); com.pingidentity.pmeyer.ws.SampleWS port = service.getSampleWSPort(); ((BindingProvider) port).getRequestContext().put("username", "user234"); ((BindingProvider) port).getRequestContext().put("password", "User234"); } } return port.hello(name); In addition we also need to modify the Web Services client SOAP Handler to use the values provided in the request context rather than the hardcoded ones. This is a single line of code change. Look for the “client.issueToken()” call and update with the following: token = client.issueToken(context.get("username").toString(), context.get("password").toString()); // token = client.issueToken(WSS_USERNAME_TOKEN_USERNAME, WSS_USERNAME_TOKEN_PASSWORD); Running the web services client now will use the credentials passed in the client application.