17
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 . So the STS endpoint is https://localhost:9031/idp/sts.wst (and /sp/sts.wst) I just have a self-signed SSL certificate so you will notice the following line in the code: stsClientConfiguration.setIgnoreSSLTrustErrors(true);

Ping Java SDK and Web Services - Ping Identity · 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

  • Upload
    hakhanh

  • View
    231

  • Download
    0

Embed Size (px)

Citation preview

Ping Java SDK and Web Services (WS-Trust) In an effort to get my head around Java Web Services I have thrown together a quickClient/Service scenario to learn how everything works. I am not the most advanced Javadeveloper and I am continually confused by all the different frameworks and interfaces thatJava provides so I wanted to build a default sandbox that I could play with to test certainweb services scenarios. I have a few scenarios here that all build from a common base. I have been using NetBeansfor 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 SAMLtoken)

Step Four: Receiving a local SAML token for the WS and returning the subject tothe web service

This may help as a learning tool or as the basics for a POC, or at least to save time fromjumping through multiple examples and sample code to produce a simple WS-Trustscenario. Pre-requisites

Java IDE with EE capable web server (ie Netbeans with Glassfish)PingFederate with STS enabledPing Java SDKPing 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 besafe, secure or well-performing by any means. Good for a POC or to learn theconcepts but nothing else.

PingFederate Configuration

For all scenarios, the following PingFederate configuration is used: Ping server is listening on https://localhost:9031. So the STS endpoint ishttps://localhost:9031/idp/sts.wst (and /sp/sts.wst) I just have a self-signed SSL certificate so you will notice the following line in thecode: 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 (eithernamed users, LDAP etc). For this sample I used the “User Table withPassword” 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 theWeb Service and to validate an existing SAML token. Configuration for thetoken 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 oursample Web Service.

Give the project a name…

I am using the built-in Glassfish server. Shouldn’t need to modify anything on this screenso 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 servicea name and a package name. Also check the box to implement as a “Stateless SessionBean”

Now we have a basic web service that will say “Hello” to a name you provide. To test theservice:

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 andselect “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 communicatewith 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 onthe application project name and select: New > Web Service Client… Choose to specify the WSDL from the Web Service project we created earlier and clickFinish to create the reference.

Now we will add this reference into the Java code to call the web service from our Javaapp. Open the Java application source file in the editor. On the Project explorer, expand the levels under the “Web Service References” until yousee the web service method (red dot) and drag that into your code (under the closing braceof 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 resultsof 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 onecan 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 thePing STS. We are going to create a SOAP Handler to handle the SAML validation withouthaving 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 tothe 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 itSampleWSHandler):

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 java.net.MalformedURLException;import java.util.Set;import javax.xml.namespace.QName;import javax.xml.soap.SOAPBody;import javax.xml.soap.SOAPException;import javax.xml.soap.SOAPFault;import javax.xml.soap.SOAPHeader;import javax.xml.soap.SOAPMessage;import javax.xml.ws.handler.MessageContext;import javax.xml.ws.handler.soap.SOAPHandler;import javax.xml.ws.handler.soap.SOAPMessageContext;import javax.xml.ws.soap.SOAPFaultException;import org.w3c.dom.Element;import 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-wss-wssecurity-secext-1.0.xsd", "Security");

if (secHeaders.getLength() == 0) { generateSOAPErrMessage(soapMsg, "No Security Header"); } Element securityHeader = (Element) secHeaders.item(0);

STSClientConfiguration stsClientConfiguration = newSTSClientConfiguration(); 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 projectexplorer, right-click the “SampleWS” service under the “Web Services” folder in the projectand 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 thatno security header was received.

Pretty cool that we only added the handler to the project. Minimal changes to the core webservice 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 viathe STS and present a SAML 2.0 assertion in the SOAP headers for the Web Service toconsume. As with the Web Service provider, in the Client project, right-click the “Libraries” folderand 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 JavaClass…

In this java file, copy the following code. You can modify the contents appropriately: package sampleclient;

import com.pingidentity.sts.clientapi.STSClient;import com.pingidentity.sts.clientapi.STSClientConfiguration;import com.pingidentity.sts.clientapi.STSClientException;import com.pingidentity.sts.clientapi.utils.StringUtils;import java.net.MalformedURLException;import java.util.Set;import java.util.TreeSet;import javax.annotation.Resource;import javax.xml.namespace.QName;import javax.xml.soap.*;import javax.xml.ws.handler.MessageContext;import 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 static String WSS_USERNAME_TOKEN_USERNAME = "user123"; private static String WSS_USERNAME_TOKEN_PASSWORD = "User123"; private static String STS_ENDPOINT_URL = "https://localhost:9031/idp/sts.wst"; private static String 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 throughto 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-wss-wssecurity-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 webservice (SampleWS) and choose “Configure Handlers…” Click the Add button and browse to the Handler we just created. Click OK to apply thehandler. Right-click the Client Project and select Run. Assuming the username/password you arepassing to the wss username token processor are correct you should be able to execute theweb service call successfully:

Now we have a web services client and server talking to each other and being secured viaSAML/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 aninvalid 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 wewant 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 javax.xml.soap.*;import javax.xml.ws.handler.MessageContext;import javax.xml.ws.handler.soap.SOAPHandler;import javax.xml.ws.handler.soap.SOAPMessageContext;import 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 com.pingidentity.sts.clientapi.STSClient;import com.pingidentity.sts.clientapi.STSClientConfiguration;import com.pingidentity.sts.clientapi.STSClientException;import com.pingidentity.sts.clientapi.SecurityTokenException;import com.pingidentity.sts.clientapi.tokens.saml.SamlToken;import com.pingidentity.sts.clientapi.tokens.saml.Saml20Token;import 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-wss-wssecurity-secext-1.0.xsd", "Security");

if (secHeaders.getLength() == 0) { generateSOAPErrMessage(soapMsg, "No Security Header"); } Element securityHeader = (Element) secHeaders.item(0);

STSClientConfiguration stsClientConfiguration = newSTSClientConfiguration(); 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 subjectout of there. (Note: there is no reason you can’t use the subject from the initial tokenreceived, 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 toretrieve that from the web services code we need to modify the web service as such: package com.pingidentity.pmeyer.ws;

import javax.annotation.Resource;import javax.jws.WebService;import javax.jws.WebMethod;import javax.jws.WebParam;import javax.ejb.Stateless;import javax.jws.HandlerChain;import javax.xml.ws.WebServiceContext;import 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 beingwelcomed:

Things to try at this point are to add attributes to the assertion and pass them through to theweb 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 andpassword: 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 = newcom.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 thevalues provided in the request context rather than the hardcoded ones. This is a single lineof 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 clientapplication.