Building Social Applications using Connections Cloud and WebSphere Portal: SAML and Single Sign-On

If you’ve been following along, we’ve created a custom solution that combines WebSphere Portal and Connection Cloud to create a socially enabled web site.  To access information in Connections Cloud, OAuth is used as the mechanism to exchange data.  Unfortunately, OAuth does not do everything.  If a user were to follow a link from Portal to Connections Cloud, he or she would need to log in to Connections Cloud.  What gives?

The reason is that WebSphere Portal is what authenticates to get the user’s data, but the actual user’s browser is not authenticated.  The result feels like a disconnect for users and non-technical observers.  The solution to this problem is SAML or Security Assertion Markup Language.  I’ve written about SAML on this blog previously, see Using SAML with SmartCloud for Social Business.  What I’ll do here is add to that work to create a solution that uses WebSphere Portal.

Design

The general flow goes a bit like this:

  1. “Something” triggers the SAML process.  It could be by a strict precondition (like right after you login) or dynamically (something realizes that you have not yet authenticated).
  2. Send the user to a web application hosted on Portal.  This web application (the actual page a user visits) is designed to construct the SAML token.
    1. The SAML token is signed using a certificate previously exchanged with IBM.  There is a manual Support process that you must follow for this to work.
  3. The web page then POSTs the SAML assertion to Connections Cloud.
    1. Connections Cloud decrypts the token, inspects the user’s identity and allows access if appropriate.

Rationale

Why would I ever do this?!?!  There are existing solutions I could use: Tivoli Federated Identity, Microsoft Active Directory Federation Server, Shibboleth.  But I needed something narrow in scope.  I don’t want an identity server – I already have one, Portal.  And I don’t want to add more servers to the existing deployment.  So I’ve created a module that does only one thing: determines who you are from Portal, creates a SAML assertion, and sends it to Connections Cloud.

That and I had the code laying around … it just needed a use case.

Implementation

This is a fairly technical project.  Even my eyes glaze over when I starting hearing about ciphers and key chains, but the main moving parts are as follows.

SAML Servlet

The SAML Servlet listens for incoming requests.  In doing so, it will do the following:

  1. Figure out the user’s identity.  The web module is protected and thus all users must be authenticated by WebShere to access.
  2. Construct the SAML token.
  3. Generate a web page with the form that sends the token to Connections Cloud.  (Javascript will submit the form automatically for the user.)
    1. There’s also a bit of code that will realize the rediret (302) coming from Connections Cloud if the admin has configured to use only this servlet as the identity server.
package com.ibm.sbt.saml.impl.servlet;
 
import java.io.IOException;
 
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import com.ibm.sbt.saml.ISamlIdentityProvider;
import com.ibm.sbt.saml.ISamlSigner;
import com.ibm.sbt.saml.SamlCreator;
import com.ibm.sbt.saml.impl.SimpleSaml11Creator;
import com.ibm.sbt.saml.impl.SimpleSamlSigner;
 
public class Saml11Servlet extends HttpServlet {
private static final long serialVersionUID = 1L;
 
private Saml11ServletConfig config;
private ISamlSigner signer;
private SamlCreator creator;
private ISamlIdentityProvider idProvider;
 
private final String emailParam = "email.domain";
 
public void init() throws ServletException {
super.init();
 
config = new Saml11ServletConfig(this.getServletConfig());
signer = new SimpleSamlSigner(config.getKeyStorePath(),
config.getKeyStorePassword(), config.getKeyStoreAlias());
creator = new SimpleSaml11Creator(config, signer);
 
// see http://www-01.ibm.com/support/knowledgecenter/SSHRKX_8.5.0/mp/dev-portlet/add_jaas.dita?lang=en
// for additional ways to get the email from a logged in user
// be careful as not all WebSphere servers use VMM (i.e. Federated Security)
idProvider = new PrincipaltoEmailIdentityProvider(this.getServletConfig().getInitParameter(emailParam));
}
 
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
 
// incoming 302 from Connections Cloud has TARGET parameter
String target = request.getParameter("TARGET");
 
String identity = idProvider.getUserIdentity(request.getUserPrincipal().getName());
String token = creator.create(identity, null);
 
response.getWriter().print(getForm(config.getEndpoint(), token, target));
}
 
private String getForm(String endpoint, String token, String target) {
return "<html xml:lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\"><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"> <title>SAML POST response</title> </head> <body> <form method=\"post\" action=\"" + endpoint + "\"><p><input name=\"TARGET\" value=\"" + target + "\" type=\"hidden\"> <input name=\"SAMLResponse\" value=\"" + token + "\" type=\"hidden\"> <noscript> <button type=\"submit\">Sign On</button> <!-- included for requestors that do not support javascript --> </noscript> </p> </form> <script type=\"text/javascript\"> setTimeout('document.forms[0].submit()', 0); </script> Please wait, signing on... </body></html>";
}
}

Identity Provider

This is pretty basic.  I’m assuming that the user currently has a Portal session.  If so, I’m grabbing the Principal (e.g. wpadmin) and then appending a configurable email domain.  The email address is a required format for Connections Cloud.  Thus you can implement your identity lookup any way you want, but the final value must be an email address that is also stored in Connections Cloud.

package com.ibm.sbt.saml.impl.servlet;
 
import com.ibm.sbt.saml.ISamlIdentityProvider;
 
public class PrincipaltoEmailIdentityProvider implements ISamlIdentityProvider {
 
	private final String domain;
 
	public PrincipaltoEmailIdentityProvider(String domain){
		this.domain = domain;
	}
 
	@Override
	public String getUserIdentity(String userId) {
		return userId + "@" + domain;
	}
}

SAML Token Creator

Things are about to get interesting.  With the user’s identity, we need to construct the SAML token.  I’ve chosen a SAML 1.1 implementation … because it was easier.  This code simply takes the template XML and substitutes appropriate values.  The result at the end is really just XML.  But this is where mistakes happen.  The values used must be accurate in not only the value but also format (e.g. date).

package com.ibm.sbt.saml.impl;
 
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SimpleTimeZone;
 
import org.apache.commons.lang.StringEscapeUtils;
 
import com.ibm.sbt.saml.ISamlConfig;
import com.ibm.sbt.saml.ISamlSigner;
import com.ibm.sbt.saml.SamlCreator;
 
public class SimpleSaml11Creator extends SamlCreator {
 
	private static final String SAML_11_TEMPLATE = "<samlp:StatusCode " + "Value=\"samlp:Success\" />$AUDIENCE$"
			+ "<saml:AuthenticationStatement " + "AuthenticationInstant=\"$AUTH_INSTANT$\" AuthenticationMethod=\"urn:oasis:names:tc:SAML:1.0:am:password\">"
			+ ""
			+ "$USER_NAME$"
			+ "urn:oasis:names:tc:SAML:1.0:cm:bearer"
			+ "<saml:NameIdentifier " + "Format=\"urn:oasis:names:tc:SAML:1.0:assertion#emailAddress\">$USER_NAME$"
			+ "urn:oasis:names:tc:SAML:1.0:cm:bearer"
			+ "$ATTRIBUTES$";
 
	public static final String ATTRIBUTE_FORMAT = "$ATTR_VALUES$";
	public static final String ATTRIBUTE_VALUE_FORMAT = "$ATTR_VALUE$";
 
	public SimpleSaml11Creator(ISamlConfig config, ISamlSigner signer) {
		super(config, signer);
	}
 
	@Override
	protected String getToken(String userId, Map<String, String[]> userAttrs) {
		Date now = new Date();
		SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT);
		Calendar cal = Calendar.getInstance(new SimpleTimeZone(0, "GMT"));
		format.setCalendar(cal);
 
		long validLength = (long) 60000 * config.getTokenExpiration();
 
		// Setup the time for which SAML assertion is valid
		Date notBefore = new Date(now.getTime() - validLength);
		Date notAfter = new Date(now.getTime() + validLength);
 
		String saml = SAML_11_TEMPLATE.replace("$ISSUE_INSTANT$",
				format.format(now));
 
		saml = saml.replace("$RESPONSE_ID$", now.getTime() + "");
		saml = saml.replace("$AUDIENCE$", config.getEndpoint());
		saml = saml.replace("$RECIPIENT$", config.getEndpoint());
		saml = saml.replace("$ISSUER$", config.getIssuer());
		saml = saml.replace("$ASSERTION_ID$", now.getTime() + "");
		saml = saml.replace("$NOT_BEFORE$", format.format(notBefore));
		saml = saml.replace("$NOT_AFTER$", format.format(notAfter));
		saml = saml.replace("$AUTH_INSTANT$", format.format(now));
		saml = saml.replace("$USER_NAME$", StringEscapeUtils.escapeXml(userId));
 
		StringBuilder allAttrs = new StringBuilder();
 
		if (userAttrs != null) {
			Iterator<Entry<String, String[]>> iterator = userAttrs.entrySet()
					.iterator();
			while (iterator.hasNext()) {
				Entry<String, String[]> entry = iterator.next();
				boolean attHasValue = false;
 
				// Setup the attribute values
				String attrValues = "";
				String[] values = entry.getValue();
				// Make sure values is not null
				if (values != null) {
					for (int j = 0; j < values.length; j++) { String value = values[j]; // Only add the attribute if there is a value to add if (value != null && value.length() > 0) {
							attHasValue = true;
							attrValues += ATTRIBUTE_VALUE_FORMAT.replace(
									"$ATTR_VALUE$",
									StringEscapeUtils.escapeXml(values[j]));
						}
					}
				}
 
				if (attHasValue) {
					// Setup the attribute name and namespace
					String key = entry.getKey();
					String attribute = ATTRIBUTE_FORMAT.replace("$ATTR_NAME$",
							StringEscapeUtils.escapeXml(key));
					attribute = attribute.replace("$ATTR_NAMESPACE$",
							StringEscapeUtils.escapeXml(key));
					attribute = attribute.replace("$ATTR_VALUES$", attrValues);
					allAttrs.append(attribute);
				}
			}
		}
		saml = saml.replace("$ATTRIBUTES$", allAttrs.toString());
 
		return saml;
	}
}

SAML Signer

Now that there’s an XML SAML token, we need to sign it.  The signer class doesn’t do the actual signing; I’ve taken care of that implementation in the SAML Creator class.  The signer class’s job is to produce the X509Certificate.  I’ve chosen to simply pull it off disk from a configurable location.  A better implementation is to get it from built-in WebSphere keystores.  And since it’s not that interesting, I’ve left it out of the blog post (though it’s in the project code).

SAML Creator

And now we need to bring it all together. Take the XML, sign it with the certificate and hand it back to the servlet for posting to Connections Cloud.

package com.ibm.sbt.saml;
 
import java.io.ByteArrayInputStream;
import java.io.StringWriter;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
 
import javax.xml.crypto.dsig.CanonicalizationMethod;
import javax.xml.crypto.dsig.DigestMethod;
import javax.xml.crypto.dsig.Reference;
import javax.xml.crypto.dsig.SignatureMethod;
import javax.xml.crypto.dsig.SignedInfo;
import javax.xml.crypto.dsig.Transform;
import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.dom.DOMSignContext;
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;
import javax.xml.crypto.dsig.keyinfo.X509Data;
import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec;
import javax.xml.crypto.dsig.spec.TransformParameterSpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
 
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
 
import com.ibm.ws.util.Base64;
 
public abstract class SamlCreator {
 
	private final Logger logger = Logger.getLogger(SamlCreator.class.getName());
 
	public static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
 
	protected ISamlConfig config;
	protected ISamlSigner signer;
 
	protected abstract String getToken(String userId,
			Map<String, String[]> userAttrs);
 
	public SamlCreator(ISamlConfig config, ISamlSigner signer) {
		this.config = config;
		this.signer = signer;
	}
 
	public String create(String userId, Map<String, String[]> userAttrs) {
		logger.fine("Creating SAML token for " + userId);
 
		String saml = getToken(userId, userAttrs);
 
		logger.finest("SAML token = " + saml);
 
		Document doc;
		try {
			DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance();
			dbfac.setValidating(false);
			dbfac.setNamespaceAware(true);
			DocumentBuilder docBuilder = dbfac.newDocumentBuilder();
			ByteArrayInputStream is = new ByteArrayInputStream(
					saml.getBytes("UTF-8"));
			doc = docBuilder.parse(is);
 
			logger.finest("Successfully converted SAML token to " + doc.getClass().getName());
 
			NodeList nodes = doc.getDocumentElement().getChildNodes();
			for (int i = 0; i < nodes.getLength(); i++) {
				// Sign the SAML assertion
				if (nodes.item(i).getNodeName().equals("saml:Assertion")) {
					if (config.signAssertion()) {
						logger.fine("Signing SAML assertion element");
						signSAML((Element) nodes.item(i), null, "AssertionID");
					} else {
						logger.fine("Skipping signing of SAML assertion element");
					}
				}
			}
 
			logger.fine("Signing SAML response");
 
			// Sign the entire SAML response
			Element responseElement = doc.getDocumentElement();
			signSAML(responseElement,
					(Element) responseElement.getFirstChild(), "ResponseID");
 
			// Transform the newly signed document into a string for encoding
			TransformerFactory fac = TransformerFactory.newInstance();
			Transformer transformer = fac.newTransformer();
			StringWriter writer = new StringWriter();
			transformer.transform(new DOMSource(doc), new StreamResult(writer));
			String samlResponse = writer.toString();
 
			logger.finest("SAML token = " + samlResponse);
 
			logger.fine("Base64 encoding SAML token");
 
			// Encode the string and return the response
			return new String(Base64.encode(samlResponse.getBytes("UTF-8")));
		} catch (Exception e) {
			e.printStackTrace();
		}
 
		return null;
	}
 
	private void signSAML(Element element, Element sibling, String referenceID) {
		logger.fine("Signing element " + referenceID);
 
		try {
// this needs to be here due to Java bug in 1.7_25
            // http://stackoverflow.com/questions/17331187/xml-dig-sig-error-after-upgrade-to-java7u25
            element.setIdAttribute(referenceID, true);
			XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
 
			DOMSignContext dsc;
			if (sibling == null) {
				dsc = new DOMSignContext(signer.getPrivateKey(), element);
			} else {
				dsc = new DOMSignContext(signer.getPrivateKey(), element,
						sibling);
			}
 
			DigestMethod digestMeth = fac.newDigestMethod(DigestMethod.SHA1,
					null);
			Transform transform = fac.newTransform(Transform.ENVELOPED,
					(TransformParameterSpec) null);
			List list = Collections.singletonList(transform);
			String refURI = "#" + element.getAttribute(referenceID);
			Reference ref = fac.newReference(refURI, digestMeth, list, null,
					referenceID);
 
			CanonicalizationMethod canMeth = fac.newCanonicalizationMethod(
					CanonicalizationMethod.EXCLUSIVE_WITH_COMMENTS,
					(C14NMethodParameterSpec) null);
			List refList = Collections.singletonList(ref);
			SignatureMethod sigMeth = fac.newSignatureMethod(
					SignatureMethod.RSA_SHA1, null);
			SignedInfo si = fac.newSignedInfo(canMeth, sigMeth, refList);
 
			KeyInfoFactory kif = fac.getKeyInfoFactory();
			List x509Content = new ArrayList();
			x509Content.add(signer.getX509Cert());
			X509Data xd = kif.newX509Data(x509Content);
			KeyInfo ki = kif.newKeyInfo(Collections.singletonList(xd));
 
			XMLSignature signature = fac.newXMLSignature(si, ki);
			signature.sign(dsc);
 
			logger.fine("Successfully signed element" + referenceID);
		} catch (Exception e) {
			// TODO: throw instead of catch
			e.printStackTrace();
		}
	}
}

Demo

If all goes well, the result should look something like this.

Download Video: MP4

 

And you’ll probably need the com.ibm.sbt.saml project code.

This code is as-is for education purposes.  FWIW I’m not a SAML expert; so if you post a question, there’s a good chance I won’t know.

Happy coding.

 

 

 

One thought on “Building Social Applications using Connections Cloud and WebSphere Portal: SAML and Single Sign-On”

Leave a Reply

Your email address will not be published.