Using cURL with IBM Connections Cloud

I love Java. But there are times that writing a program is more work than it’s worth.  And to the novice, trying to get set up with a JVM, IDE, etc only adds to the time commitment.

So I re-introduce you to cURL (I’ve mentioned it a few times on the blog).  What is cURL?  It’s like a browser – only without the user interface.  cURL gets and sends the raw text data to and from a server.  This is what you see when you use the “View Source” option in your web browser.

I’ll use cURL to populate a bunch of Connections Cloud communities quickly. (You could do this for on-premises as well.)  For example, let’s say my company just moved to Connections Cloud. And for every network shared folder we previously used to be organized (terrible), we’d rather use a Connections Cloud community (awesome).  The reason to leverage cURL to do this is that creating the community is very easy. And it’s something you’ll do once or occasionally.  So a scripted approach is more efficient than writing code.

Let’s get to it.  For reference, review the cURL scripts I have laying around in cURLConnectionsCloud.zip. Just unzip it to any Windows computer.

cURL

You can either download cURL or use the one I’ve packaged in my cURLConnectionsCloud.zip.  I’d recommend using mine since it works with the rest of the examples.

Setup

Every cURL script I create starts with some setup to initialize parameters like server URL, username, and password.  The first time you run the scripts, it will prompt for user name and password.  Anything run subsequently will be done in the context of this user name (e.g. My Communities).

SetupCurl.bat

The below command sets the path to the cURL executable.  It also ensures that basic authentication is used and the username:password pair are included any time a Connections Cloud script is run.

set curl=%~dp0/curl/curl.exe -L -k -u %cnx_username%:%cnx_password%

SetupCnx.bat

This script set the URL to the server.  It also prompts the user for credentials if not already provided previously.

@echo off
REM CA1 Test Server
REM set cnx_url=https://apps.collabservnext.com
REM North America Production Server
set cnx_url=https://apps.na.collabserv.com
IF DEFINED cnx_url (echo %cnx_url%) ELSE (set /p cnx_url= Connections URL:)
IF DEFINED cnx_username (echo %cnx_username%) ELSE (set /p cnx_username= Connections ID:)
IF DEFINED cnx_password (echo **masked**) ELSE (set /p cnx_password= Connections Password:)

Usage

Next we need to create a community.  This is done simply by sending text to the Connections Cloud server.

The Script

The cURL script looks like the following.

@echo off
call ../SetupCnx.bat
call ../SetupCurl.bat
%curl% -v -X POST --data-binary @%1 -H "Content-Type: application/atom+xml" %cnx_url%/communities/service/atom/communities/my

A couple of points:

  • -v is the verbose flag; I use it to see everything that happens. You can remove it if you’d like
  • –data-binary @%1 means that I am sending a file to the server and the file name is provided as input on the command line
  • -H “Content-Type: application/atom+xml” is a required setting; you need to set a header specifying the content type per the API doc
  • %cnx_url%/communities/service/atom/communities/my is the URL to the Connections endpoint per the API doc

To create the community, all that’s needed is to create an XML file and run the following command.

C:\IBM\workspaces\connections\cURL\communities>CreateCommunity.bat CommunityInpu
t.xml

The Input

The above command has CommunityInput.xml at the end.  This is the input file that is used to create the community. The input XML file is easy on the eyes as well.  If we had multiple communities, I would write a few more lines in the script to substitute the list of folders for the title field.  Or you could create more input files … it’s a lot easier to edit text than program.

<?xml version="1.0" encoding="UTF-8"?>
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app"
 xmlns:snx="http://www.ibm.com/xmlns/prod/sn">
 <title type="text">Community Name Goes Here</title>
 <content type="html">Community Description Goes Here</content>
 <author>
 <name>Van Staub</name>
 <email>van_staub@us.ibm.com</email>
 <snx:userid>20002888</snx:userid>
 <snx:userState>active</snx:userState>
 </author>
 <contributor>
 <name>Van Staub</name>
 <email>van_staub@us.ibm.com</email>
 <snx:userid>20002888</snx:userid>
 <snx:userState>active</snx:userState>
 </contributor>
 <category term="community" scheme="http://www.ibm.com/xmlns/prod/sn/type"></category>
 <snx:communityType>public</snx:communityType>
</entry>

I’ve boldfaced the areas you might want to change. But use the API doc as a guide of what you can additionally set.  Most importantly the snx:userid applies to either your GUID for Connections on-premises or your subscriber ID for Connections Cloud.

That’s it.

  1. Unzip my sample.
  2. Update the CommunityInput.xml.
  3. Run CreateCommunity.bat

So next time you need to get something completed quickly or just want to experiment with the APIs, take a look at the cURL scripts I posted.  Most of them should work …

Happy scripting!

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 "&lt;html xml:lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\"&gt;&lt;head&gt;&lt;meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"&gt; &lt;title&gt;SAML POST response&lt;/title&gt; &lt;/head&gt; &lt;body&gt; &lt;form method=\"post\" action=\"" + endpoint + "\"&gt;&lt;p&gt;&lt;input name=\"TARGET\" value=\"" + target + "\" type=\"hidden\"&gt; &lt;input name=\"SAMLResponse\" value=\"" + token + "\" type=\"hidden\"&gt; &lt;noscript&gt; &lt;button type=\"submit\"&gt;Sign On&lt;/button&gt; &lt;!-- included for requestors that do not support javascript --&gt; &lt;/noscript&gt; &lt;/p&gt; &lt;/form&gt; &lt;script type=\"text/javascript\"&gt; setTimeout('document.forms[0].submit()', 0); &lt;/script&gt; Please wait, signing on... &lt;/body&gt;&lt;/html&gt;";
}
}

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 = "&lt;samlp:StatusCode " + "Value=\"samlp:Success\" /&gt;$AUDIENCE$"
			+ "&lt;saml:AuthenticationStatement " + "AuthenticationInstant=\"$AUTH_INSTANT$\" AuthenticationMethod=\"urn:oasis:names:tc:SAML:1.0:am:password\"&gt;"
			+ ""
			+ "$USER_NAME$"
			+ "urn:oasis:names:tc:SAML:1.0:cm:bearer"
			+ "&lt;saml:NameIdentifier " + "Format=\"urn:oasis:names:tc:SAML:1.0:assertion#emailAddress\"&gt;$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&lt;String, String[]&gt; 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&lt;Entry&lt;String, String[]&gt;&gt; iterator = userAttrs.entrySet()
					.iterator();
			while (iterator.hasNext()) {
				Entry&lt;String, String[]&gt; 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 &lt; values.length; j++) { String value = values[j]; // Only add the attribute if there is a value to add if (value != null &amp;&amp; value.length() &gt; 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&lt;String, String[]&gt; userAttrs);
 
	public SamlCreator(ISamlConfig config, ISamlSigner signer) {
		this.config = config;
		this.signer = signer;
	}
 
	public String create(String userId, Map&lt;String, String[]&gt; 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 &lt; 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.

 

 

 

Building Social Applications using Connections Cloud and WebSphere Portal: SBTSDK

To build social applications, I strongly suggest using the Social Business Toolkit SDK.  While Connections Cloud has simple to use REST APIs, the SDK provides a ready to go foundation.  Admittedly, the SDK is large and can be confusing at first.  In it you’ll find code for a variety of technology platforms: Domino, iOS, PHP, Java, Javascript.  We, developers, tend to want to jump right in and start code.  Don’t.  If you don’t start with the SDK, you’ll find yourself building authentication mechanisms, HTTP clients, XML readers, etc … stuff you really don’t need to re-invent.

There are two ways to get the Social Business Toolkit SDK:

  • Download the SDK and sample web applications from OpenNTF.   Since the SDK and samples are pre-packaged, this allows you to simply install and get going.
  • Download the SDK source and build the projects from GitHub.  While this takes more time, the code is more recent.  The last package posted on OpenNTF is over a year old at the time of writing.

Downloading the Social Business Toolkit SDK from OpenNTF

To download and install, see documentation.  High level steps are:

  1. Download the latest version of the SDK.
  2. Extract the zip and locate the file \samples\ear\sbt.sample-1.0.3.20140723-1200.ear (or similar name).

Building the Social Business Toolkit SDK from GitHub

The documentation on building the source is a bit dated.  Most developers will be able to follow the steps below.

  1. Download the source (e.g. download ZIP) from the SocialSDK repo on SBTSDK GitHub.
  2. Import the maven projects into Eclipse.  The Import -> Maven -> Existing Maven Projects is an option in later versions of Eclipse.  If you do not have this, consider downloading the M2Eclipse plugin.
  3. Depending on how you import, you may need to update the Project Explorer view to include the com.ibm.sbt working set.Maven SBT Working Set
  4. Select the sbt.sample project -> Export -> EAR File.

Installing the Social Business Toolkit SDK Sample

  1. Install the EAR (either the one from Downloading Step 2 or Building Step4) using WAS admin console.  I’m installing the SDK directly to the Portal server.
  2. Verify the application by visiting http://<portal>:<port>/sbt.sample.web/ in a web browser.  (The default port for Portal is 10039.)

SBT Home

 

Congratulations, now you can get started.

Building Social Applications using Connections Cloud and WebSphere Portal: Your First Social Portlet

Part of the series Building Social Applications using Connections Cloud and WebSphere Portal.

Create a Sample Application

Let’s start simple.  We’ll re-use on the SBT SDK’s sample applications inside Portal.

  1. Go to the SBT SDK application http://<portal>:<port>/sbt.sample.web/javascript.jsp.
  2. Authenticate using OAuth if you did not do so previously.  Do this by clicking the “Login” button on the Authentication -> Authentication Summary sample.
  3. Next select the Social -> Files -> Get My Files sample.
  4. If an error occurs, update line 18 to include the smartcloudOA2 endpoint in the service.
    var fileService = new FileService({endpoint: "smartcloudOA2"});
  5. Click the “Run” button.  You should see a list of files.  (If not, just make sure there are actually files in the My Files area of Connections Cloud for this user.)
  6. Keep the sample open, we’ll use it in Portal.

SBT My Files Sample

 

Adding the Sample to the Script Portlet

Now we’ll take the same code seen in the Javascript tab and add that to the Script Portlet.  The effect is exact same as the Get My Files sample – only that it’s coming from Portal.

  1. Log in as an administrator or a user with edit rights to Portal pages.
  2. Create a new page in Portal.
  3. Add the Script Portlet to the page from the content palette.  (If you get an error when doing this, confirm the site mapping for the page in the steps below.)
    1. In the Page Properties, select Web Content -> Edit.
    2. Click “Add Web Content”.
    3. Navigate to Libraries -> Script Portlet Library -> Script Portlet Applications.
    4. “OK” and re-add the Script Portlet to the page.
  4. Click “Edit” in the Script Portlet.
  5. Fill in the HTML and Javascript tabs in the Script Portlet with the respective tabs in the SBT SDK sample.Script Portlet JS
  6. Open a new browser tab and navigate to http://<portal>:<port>/sbt.sample.web/library.  You will receive Javascript as a response.  Add this Javascript to the beginning of the Javascript in the Script Portlet.
  7. Save the Script Portlet.
  8. Exit “Edit Mode” to view the page.

You should now see the same list of files that you saw in the SBT SDK sample on your Portal page.  It may not be pretty, but it is functional.  Try copying other examples from the SDK sample application to Script Portlets.

Get My Files Portal

Some may be wondering why I needed to copy the Javascript located at http://<portal>:<port>/sbt.sample.web/library into the Script Portlet.  This is required to:

  • Ensure the endpoint smartcloudOA2 is available
  • Set the AMD paths to the SDK’s modules.  If this was not done, we’d see the SDK trying to load modules from a Portal context.  Rather they must be loaded from the SDK enterprise application.

Copying this directly into the Script Portlet isn’t ideal.  What if we have two Script Portlets, do we copy into both?  One solution is to include this requirement as an external Javascript resource by adding the http://<portal>:<port>/sbt.sample.web/library URL to the list of dependencies.

SBT Dependency

Another solution I’ve found is use Portal’s page profiles to specify when this bit of code needs to load.  Thus when a Script Portlet containing the SBT SDK exists on the page, I change the page’s profile such that it loads this required Javascript.  I’ll save that discussion for a more advanced post.

 

 

Building Social Applications using Connections Cloud and WebSphere Portal

In this series, I’ll explore creating social applications using WebSphere Portal and Connections Cloud.  Specifically, I’ll focus on leveraging the following IBM products and resources:

  • WebSphere Portal (or IBM Web Content Manager)
  • Connections Cloud
  • the Social Business Toolkit SDK
  • WebSphere Portal’s new Script Portlet

To be clear, there are various ways to create a social application in Portal.  Consider reviewing the Redbook Building and Implementing a Social Portal for other options.  If you’d like an IBM off-the-shelf solution, read the Redbook.  But if you’d like to see an alternative approach, follow along:

  1. Downloading and Building the Social Business Toolkit SDK
  2. Getting Started
  3. Your First Social Portlet
  4. Social Portal Pages
  5. Single Sign-On with SAML
  6. Chat-as-a-Service (Coming Soon)

Building Social Applications using Connections Cloud and WebSphere Portal: Getting Started

Part of the series Building Social Applications using Connections Cloud and WebSphere Portal.

Installing the Script Portlet

We’ll be using WebSphere Portal’s Script Portlet.  The reason is that the Script Portlet is ideal for small, web-centric applications.  Much of the Social Business Tookit’s examples are exactly that – small, web-centric apps.  So it will be easy to use the Script Portlat and Toolkit as a starter for social applications.  Also by using the Script Portlet, the technical barrier to creating these applications is lower – assuming you don’t live and breath J2EE portlet development.

If you do not already have the Script Portlet installed, see documentation.  High level steps are:

  1. Download the Script Portlet from the Greenhouse Catalog.
  2. Unpack the downloaded zip and move the scriptportlet-app-1.3.0.paa file to the Portal server.
  3. Run
    ./ConfigEngine.sh install-paa -DPAALocation=/path/scriptportlet-app-1.3.0.paa -DWasPassword=password -DPortalAdminPwd=password
  4. Run
    ./ConfigEngine.sh deploy-paa -DappName=scriptportlet-app -DWasPassword=password -DPortalAdminPwd=password
  5. Restart Portal.

Configuring Connections Cloud

The Toolkit will use OAuth to communicate with Connections Cloud and retrieve data.  To do that, you’ll need to add an “Internal App” in Connections Cloud.

  1. Log in as an administrator or app developer to Connections Cloud (usually https://apps.na.collabserv.com/manage/account/isv/input).
  2. Click Internal Apps -> Register App.
  3. Provide a name and select the OAuth 2.0 radio button.
  4. Set the callback URL to the server where you installed the Toolkit.  For example, my server is http://portal.demos.ibm.com/sbt.sample.web/service/oauth20_cb.
  5. Click Register.
  6. Back on the Internal Apps page, select the drop down for the app you created.
  7. Click “Show Credentials” and click the “Show Client Secret” link.
  8. Leave this screen open; the details will be used next.

OAuth2.0 Settings

Configuring the Social Business Toolkit SDK

Previously, we installed the SBT SDK.  Now it must be configured to work with the Internal App we created.

  1. Using a text editor, create the file sbt.properties.
  2. Copy the following into the sbt.properties file.  You will need to update the section “SmartCloud OAuth 2.0 Endpoint Parameters” with settings from the “Show Credentials” screen on the “Internal Apps” page.
    1. # IBM Social Business Toolkit Configuration
      # Library Servlet Configuration
      environments=Default:defaultEnvironment
      
      # SmartCloud OAuth 2.0 Endpoint Parameters
      smartcloudOA2.url=https://apps.na.collabserv.com
      smartcloudOA2.serviceName=LotusLive
      smartcloudOA2.appId=Oauth2Impl
      smartcloudOA2.consumerKey=app_20002887_143441864494
      smartcloudOA2.consumerSecret=4436e061cbd947c9490be505e3de9f0a5eff0283dd7987b5c5b9b91229ff4f8f38f2bbfa59f50eff6d993f29ccc53d4ec6153d45559a62e1848f47fc70336362fcb4d9e6822389377d8a1c80f61b95182f7d77c988331b4320a949964f1a55a1bc23c854d41f4d6e529c6acf3af760e515e5b52183f9f5e8f3bc0705c0b2e9
      smartcloudOA2.authorizationURL=https://apps.na.collabserv.com/manage/oauth2/authorize
      smartcloudOA2.accessTokenURL=https://apps.na.collabserv.com/manage/oauth2/token
      smartcloudOA2.apiVersion=3.0
  3. Since this tutorial focuses on Connections Cloud and OAuth, we’ll remove the SDK’s default environments and additional authentication options for clarity.
    1. Create a file called managed-beans.xml with any text editor.
    2. Copy the following into the file.
      <?xml version="1.0"?>
      <faces-config>
      <!-- Credential store physical implementation -->
      <managed-bean>
      <managed-bean-name>CredStore</managed-bean-name>
      <managed-bean-class>com.ibm.sbt.security.credential.store.MemoryStore</managed-bean-class>
      <managed-bean-scope>application</managed-bean-scope>
      </managed-bean>
      
      <!-- Default Environment -->
      <managed-bean>
      <managed-bean-name>defaultEnvironment</managed-bean-name>
      <managed-bean-class>com.ibm.sbt.jslibrary.SBTEnvironment</managed-bean-class>
      <managed-bean-scope>application</managed-bean-scope>
      <managed-property>
      <property-name>endpoints</property-name>
      <value>smartcloudOA2</value>
      </managed-property>
      <managed-property>
      <property-name>properties</property-name>
      <value>sample.email1,sample.email2</value>
      </managed-property>
      <managed-property>
      <property-name>runtimes</property-name>
      <value>smartcloudOA2</value>
      </managed-property>
      </managed-bean>
      
      <!-- SmartCloud OAuth 2.0 -->
      <managed-bean>
      <managed-bean-name>smartcloudOA2</managed-bean-name>
      <managed-bean-class>com.ibm.sbt.services.endpoints.SmartCloudOAuth2Endpoint</managed-bean-class>
      <managed-bean-scope>session</managed-bean-scope>
      <!-- Endpoint URL -->
      <managed-property>
      <property-name>url</property-name>
      <value>%{smartcloudOA2.url}</value>
      </managed-property>
      <managed-property>
      <property-name>apiVersion</property-name>
      <value>%{smartcloudOA2.apiVersion}</value>
      </managed-property>
      <managed-property>
      <property-name>serviceName</property-name>
      <value>%{smartcloudOA2.serviceName}</value>
      </managed-property>
      <!-- OAuth parameters -->
      <managed-property>
      <property-name>appId</property-name>
      <value>%{smartcloudOA2.appId}</value>
      </managed-property>
      <managed-property>
      <property-name>consumerSecret</property-name>
      <value>%{smartcloudOA2.consumerSecret}</value>
      </managed-property>
      <managed-property>
      <property-name>consumerKey</property-name>
      <value>%{smartcloudOA2.consumerKey}</value>
      </managed-property>
      <managed-property>
      <property-name>authorizationURL</property-name>
      <value>%{smartcloudOA2.authorizationURL}</value>
      </managed-property>
      <managed-property>
      <property-name>accessTokenURL</property-name>
      <value>%{smartcloudOA2.accessTokenURL}</value>
      </managed-property>
      <!-- Trust the connection -->
      <managed-property>
      <property-name>forceTrustSSLCertificate</property-name>
      <value>true</value>
      </managed-property>
      <!-- Access to the credential store -->
      <managed-property>
      <property-name>credentialStore</property-name>
      <value>CredStore</value>
      </managed-property>
      </managed-bean>
      </faces-config>
      
    3. Save.
  4. Copy the sbt.properties and managed-beans.xml file to the server.  For example, /opt/IBM/WebSphere/wp_profile/sbt.  (I created the sbt directory.)
  5. Create two new JNDI URLs in WebSphere.
    1. Access the WAS admin console.
    2. Navigate to Resources -> URL -> URLs.
    3. Select the scope to be your cell.
    4. Select New to create a URL with the following properties:
      1. Name=SBT Properties
      2. JNDI name=url/ibmsbt-sbtproperties
      3. Specification=file:///opt/IBM/WebSphere/wp_profile/sbt/sbt.properties
    5. Select New to create another URL with the following properties:
      1. Name=SBT Managed Beans
      2. JNDI name=url/ibmsbt-sbtproperties
      3. Specification=file:///opt/IBM/WebSphere/wp_profile/sbt/managed-beans.xml
    6. Save.
  6. Restart the “Social Business Toolkit Sample Enterprise Application” under Enterprise Applications in WAS.

Test the Social Business Toolkit SDK

Time to test whether the above steps worked.  To do that:

  1. Go to http://<portal>:<port>/sbt.sample.web/javascript.jsp in a web browser.
  2. Select the Authentication -> Authentication Summary sample.
  3. Click the “Login” button.
  4. You should see a Connections Cloud login screen.
  5. Provide valid credentials and click “Log In”.
  6. The previous “Login” button should change to “Logout”

OAuth Authenticated

What’s Next

Next, we’ll create a simple application with the Script Portlet and the Social Business Toolkit SDK examples.

Building Social Applications using Connections Cloud and WebSphere Portal: Your First Widget

IBM Verse Application Development

UPDATE

IBM finally released Verse extension points: business card and view/compose widget integration.  See What’s New October 2016.

Technical details can be found here.

ORIGINAL CONTENT

On March 31, IBM released its much anticipated solution to social and email, IBM Verse. And part of my role in IBM is demonstrating how business partners can use technology like IBM Verse to build new and engaging products. The IBM team is still in the process of publishing development material. For now, take a look at the Tech Talk replay “A Deeper Look into IBM Verse“. I’ve also created my own presentation on Verse application development. We are currently giving this presentation as part of a one day workshop in the US. If you’d like to discuss further, feel free to contact me.

Verse Application Development

The presentation is framed as what we have today and what is coming. I’ll talk about what we have today below. But alot is coming, and much of the presentation also discusses how we foster application development in our cloud so that when those Verse APIs are released, you can begin using them quickly.

So what’s available today?

Mailto: “API”

I have quotes around API because application developers might argue that this is not API – though it is one way to immediately integrate Verse.  Mailto is a link in a web page. You’ve no doubt seen the Contact Us link that launches your Notes or Outlook desktop client. Those same web links will work with Verse. Web developers can create links that begin a draft email in Verse and optionally add a recipient, subject, and body. For details, see this post.

Verse Search API

OK, now this one is API. It’s also the same way the Verse web client works. When you click a person, you’ll only see emails from that person. This is called a facet and allows you to filter out all the stuff you don’t care about. So you can concentrate on reviewing emails using:

  • People
  • With Attachments
  • With Links
  • Needing Action
  • Waiting For
  • Date Ranges

Your custom application can filter and receive responses in much the same way. To figure out how (since there’s no public API yet), look at the network tool in Chrome, Firefox, etc. And then just start clicking on the facets to see how the URLs are constructed.

IBM Verse Search API

 

In this screenshot, I’ve just clicked on the Needs Action facet. And I’m given an email from Amy that I’ve flagged as needing my follow up. The URL requests denoted by the lower red arrows show the API calls being made. Below is the actual GET request.

https://mail.notes.na.collabserv.com/api/search/documents?wt=json&df=body&q.op=AND&TZ=America%2FNew_York&dbfile=livemail&q=((((replydate%3A*%20OR%20followupstatus%3A%3F)%20AND%20!folderunid%3A%22FFFFFFFF000000000000000000000001%22%20AND%20!folderunid%3A%22FFFFFFFF000000000000000000000002%22))%20(softdeletion%3A0))%20OR%20unid%3A(CED442931844E0E700257E1300089C78)&start=0&rows=31&withunread=1&sort=followupdate%20asc%2Cmaildate%20desc&group=true&group.field=tua0&group.sort=maildate%20desc&group.limit=1&xhr=1

Go ahead and paste the URL in your address bar, you should get back data.  The API endpoint is https://mail.notes.na.collabserv.com/api/search/documents.  And it carries a set of parameters that define the search:

  • wt:json
  • df:body
  • q.op:AND
  • TZ:America/New_York
  • dbfile:livemail
  • q:((((replydate:* OR followupstatus:?) AND !folderunid:”FFFFFFFF000000000000000000000001″ AND !folderunid:”FFFFFFFF000000000000000000000002″)) (softdeletion:0)) OR unid:(CED442931844E0E700257E1300089C78)
  • start:0
  • rows:31
  • withunread:1
  • sort:followupdate asc,maildate desc
  • group:true
  • group.field:tua0
  • group.sort:maildate desc
  • group.limit:1
  • xhr:1

The “q” parameter is the important one. As you click on facets, note how this parameter changes.

And what you’ll get back is a JSON response that you can use in your custom application.  I’ll save explanation of the parameters, request, and response format for the real IBM documentation. But armed with this, you can start investigating and probably get a good idea of how to search and retrieve information from Verse.

What Else?

So what else can you do? I show some of what’s coming in the presentation. But think about the information stored in Verse: your todo list, who’s important to you, email and social content. Because it provides so many possibilities to improve not only email but other applications, this is why Verse really is a #NewWayToWork.

 

Connections Cloud Navigation Tricks

There are some rather neat navigational tricks that you can use inside IBM Connections Cloud. Unfortunately these tricks are undocumented and as such are likely to be “unsupported” by IBM and could possibly change in the future.  But because I find them so useful, I just have to show you. And hopefully over time, they find their way into product documentation and API.

Top Navigation Bar Extensions

There is a documented way to add new links to the navigation bar in Connections Cloud. This is done through an extension point where you define the text, link, icon, and other properties of the link. By following these instructions, you can add a link to your company’s expense reporting system, or scheduling, or whatever. The problem comes in when you want a menu of links, or rename this default link to something else, or just want something a little more sophisticated.

A Real World Need

So what might be an example of something more sophisticated. Let’s assume you are using communities to group concepts in your company. Some people have communities devoted to clients. Others have communities for specific projects. And if you’re really organized, you have client communities with projects as sub-communities. The problem arises when you go to look at those communities.

Flat Communities

Because communities are listed all together, you lose the semantics. Which ones are my projects and which ones are my clients? So you begin to use tricks like tagging. My client communities are tagged with “client” and projects are “project” tagged. This helps, but you’ll still start with the full list and need to refine based on tags. Not terrible – in fact this is a great start. But we can do better.

Tags, Semantic Glue

If you were to click the tag “client”, you would be looking at all communities that are effectively clients.

Client Communities

The link in your address bar looks like this.

https://apps.collabservnext.com/communities/service/html/allcommunities#tag=client&sortKey=update_date&sortOrder=desc&page=0&numItems=10

And you can even look at “My Clients” by using a link to communities you are a member (or own) with the tag “client”.

https://apps.collabservnext.com/communities/service/html/mycommunities#tag=client&sortKey=update_date&sortOrder=desc&page=0&numItems=10

This is neat because we have the semantics of what we want through the links (aided by tags). We just need to go one step farther to provide direct access.

Navigation Tricks

Here’s where it gets really interesting. I want two menus

  1. The first will list “Clients” with a submenu of “My Clients” and “All Clients”
  2. The second follows the same convention but will be for projects

To do this, we need to use a special syntax in the Organizational Extension’s name. The red arrow is the secret sauce. The blue arrows show values that go unused but still need to be populated to save the extension.

Org Extension Trick

We create two of these extensions for both Clients and Projects. The result is two new menus in our navigation bar. Notice that Projects has the submenu with My Projects and All Projects. And clicking My Projects takes you to the communities application that list your communities and then applies the “project” tag. We’re just enforcing the semantics in a dont-make-me-think type way.

New Navigation Menus

So let’s look at that extension trick in more detail. Here is what’s entered in the name field.

@@mod@auth-left.servicesMenu@after@{"id" : "Projects", "topMenu" : "ProjectsMenu", "text" : "Projects", "class" : "feature", "iconUrl": "", "items" : [{ "url" : "https://apps.collabservnext.com/communities/service/html/mycommunities#tag=project&sortKey=update_date&sortOrder=desc&page=0&numItems=10", "text" : "My Projects"}, { "url" : "https://apps.collabservnext.com/communities/service/html/allcommunities#tag=project&sortKey=update_date&sortOrder=desc&page=0&numItems=10", "text" : "All Projects"}]}

This looks really really esoteric so let’s break it down.

  1. The beginning is the secret syntax to add a new menu at the end of the navigation bar: @@mod@auth-left.servicesMenu@after@{“id” : “Projects”, “topMenu” : “ProjectsMenu”, “text” : “Projects”, “class” : “feature”, “iconUrl”: “”
  2. Next we have a list of items that are the submenu links: “items” : [{ … }]
  3. And each of those submenu links has the URL to the communities application with the tag parameter added: { “url” : “https://apps.collabservnext.com/communities/service/html/mycommunities#tag=project&sortKey=update_date&sortOrder=desc&page=0&numItems=10”, “text” : “My Projects”}

That’s all there is to it. The really observant person might also see that there is no more “Communities” link. That’s because I removed it – favoring the “Clients” and “Projects” direct access over generic “Communities”. To remove the communities application, you create another organization extension with the following name .

@@mod@auth-left.communitiesMenu@delete@

Final Thoughts

You can use these tactics to remove, rename, and create more intuitive navigation in your Connections Cloud. Or add more tags and facets to build additional semantics. For example to find out what those tags or facets are, use Connections Cloud’s advanced search to find the content you want. Then review the address bar. It will contain the URL with parameters needed in the extension.

Just remember that these changes will apply to everyone in the company. And you’ll want to Ctrl+F5 to clear your browser cache to observe the changes.

Using SAML with SmartCloud for Social Business

SAML, Security Access Markup Language, provides an external application (e.g. a partner’s website) the ability to assert the identity of a SmartCloud user.  Prior to any interaction between a partner’s application and SmartCloud, a process is initiated by the IBM customer service team to enable SAML.  This entails sharing an SSL certificate from the partner’s application with SmartCloud.  By doing so, a partner’s application can now securely encrypt data that states the identity of a user.  In short, the partner’s application is saying, “This user is tamado@demos.ibm.com … Trust me.”  So long as SmartCloud can decrypt the sent data using the SSL certificate provided by the partner, it trusts the assertion of the user’s identity.

If this seems like the enterprisey concept of Single Sign-On, it is.  One can be authenticated on a partner’s application and be sent to SmartCloud without a need to re-login.  But the solution begets other advantages.

Consider what’s normally used to login to SmartCloud, your email address and password.  Those credentials may be different from legacy systems.  For example for many years communication services providers (CSPs) used a combination of user ID and PIN.  Having them now use an email address and password could be a problem for those that have been trained to use their ID/PIN combo.  To alleviate this concern, SAML can be used as the intermediary.  Using the existing ID/PIN pair in a partner’s login form, the partner can now associate ID 12345 with tamado@demos.ibm.com and provide the email identity to SmartCloud.  The only change needed is a new technical ability to map an existing ID to the respective SmartCloud email address.  To the user, nothing has changed.

SAML also provides a mechanism to specify where the user should go inside SmartCloud after login.  Again, our CSPs are heavily focused on Meetings. Rather than start your experience on the homepage (dashboard) in SmartCloud, you could be sent directly into hosting a meeting without a need to authenticate.

This now becomes a rather technical conversation.  First, it begins with the exchange of information – namely the SSL certificate – with the SmartCloud team.  See the technote Enabling Federated Identity or Integration Server for use with SmartCloud for Social Business for details.  Next we proceed to the following SAML workflow.

SAML ProcessIt begins with the user requesting authentication on the partner’s login form.  I’ve created the following form.  All it asks me to do is provide my email address – it’s a very trusting login form. The point is that the end result of the partner’s login process should produce the understanding that this user is tamado@demos.ibm.com. Here I’m just asking for that understanding more directly (i.e. tell me who you are).

SAM FormNext, we must create the SAML assertion (an XML document) that contains the identity of the user and is signed accordingly.  For this demonstration, I’ve used an open source PHP module called xmlseclibs to complete the encryption.  Since the syntax and format of the XML document is very specific, I’ve created the following helper class.

class SmartCloudToken {
 
	const TOKEN_EXPIRATION_MINUTES = 60;
 
	private $key = NULL;
	private $cert = NULL;
	private $isFile = FALSE;
 
	public function __construct($key, $cert, $isFile = FALSE) {
		$this->key = $key;
		$this->cert = $cert;
		$this->isFile = $isFile;
	}
 
	private function getTokenTimestamps() {
		$time = gmmktime();
 
		$instant = gmstrftime("%Y-%m-%dT%H:%M:%S.000Z", $time);
 
		// compute the time conditions under which the SAML token is valid
		$notBefore = gmstrftime("%Y-%m-%dT%H:%M:%S.000Z", ($time - (60 * SmartCloudToken::TOKEN_EXPIRATION_MINUTES)));
		$notAfter = gmstrftime("%Y-%m-%dT%H:%M:%S.000Z", ($time + (60 * SmartCloudToken::TOKEN_EXPIRATION_MINUTES)));
 
		return array("epoch"=>$time,
				"instant" => $instant,
				"notBefore" => $notBefore,
				"notAfter" => $notAfter);
	}
 
	public function getToken($recipient, $issuer, $email) {
		$xml = $this->getTokenXML($recipient, $issuer, $email);
		return $xml->saveXML();
	}
 
	private function getTokenXML($recipient, $issuer, $email) {
		$xml = <<<XML
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol"
    IssueInstant="%ISSUE_INSTANT%"
    MajorVersion="1"
    MinorVersion="1"
    Recipient="%RECIPIENT%"
    ResponseID="Response-%RESPONSE_ID%" >
    <samlp:Status>
        <samlp:StatusCode Value="samlp:Success" />
    </samlp:Status>
    <saml:Assertion
        xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion"
        AssertionID="Assertion-%RESPONSE_ID%"
        IssueInstant="%ISSUE_INSTANT%"
        Issuer="%ISSUER%"
        MajorVersion="1"
        MinorVersion="1" >
        <saml:Conditions
            NotBefore="%NOT_BEFORE%"
            NotOnOrAfter="%NOT_AFTER%" >
            <saml:AudienceRestrictionCondition>
                <saml:Audience>%AUDIENCE%</saml:Audience>
            </saml:AudienceRestrictionCondition>
        </saml:Conditions>
        <saml:AuthenticationStatement
            AuthenticationInstant="%AUTH_INSTANT%"
            AuthenticationMethod="urn:oasis:names:tc:SAML:1.0:am:password" >
            <saml:Subject>
                <saml:NameIdentifier Format="urn:oasis:names:tc:SAML:1.0:assertion#emailAddress">%USER_NAME%</saml:NameIdentifier>
                <saml:SubjectConfirmation>
                    <saml:ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:bearer</saml:ConfirmationMethod>
                </saml:SubjectConfirmation>
            </saml:Subject>
        </saml:AuthenticationStatement>
        <saml:AttributeStatement>
            <saml:Subject>
                <saml:NameIdentifier Format="urn:oasis:names:tc:SAML:1.0:assertion#emailAddress">%USER_NAME%</saml:NameIdentifier>
                <saml:SubjectConfirmation>
                    <saml:ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:bearer</saml:ConfirmationMethod>
                </saml:SubjectConfirmation>
            </saml:Subject>
        </saml:AttributeStatement>
    </saml:Assertion>
</samlp:Response>
XML;
		$timestamps = $this->getTokenTimestamps();
 
		// replace the variable XML values with correct SAML values
		$template = array("%ISSUE_INSTANT%", "%RESPONSE_ID%", "%AUDIENCE%", "%RECIPIENT%", "%ISSUER%", "%ASSERTION_ID%", "%NOT_BEFORE%", "%NOT_AFTER%", "%AUTH_INSTANT%", "%USER_NAME%");
		$computed = array($timestamps['instant'], $timestamps['epoch'], $recipient, $recipient, $issuer, $timestamps['epoch'], $timestamps['notBefore'], $timestamps['notAfter'], $timestamps['instant'], $email);
 
		$xml = str_replace($template, $computed, $xml);
 
		$doc = new DOMDocument();
		$doc->loadXML($xml);
 
		// the URI in the signed reference node must match the ResponseID value, which is set as the epoch above
		$this->sign($doc->documentElement, "ResponseID", "Response-" . $timestamps['epoch']);
 
		// unless configured by IBM, no need to sign the assertion
		//$this->sign($doc->getElementsByTagName("Assertion")->item(0), "AssertionID", "Assertion-" . $timestamps['epoch'], FALSE);
 
		return $doc;
	}
 
	private function sign($node, $refId, $uri, $top = TRUE){
		$objDSig = new XMLSecurityDSig();
		$objDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N_COMMENTS);
		$objDSig->addReference2($node, XMLSecurityDSig::SHA1, array('http://www.w3.org/2000/09/xmldsig#enveloped-signature'), $refId, $uri);
		$objKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA1, array('type'=>'private'));
 
		if($this->isFile){
			$objKey->loadKey($this->key, TRUE);
		} else {
			$objKey->loadKey($this->key);
		}
 
		$objDSig->sign($objKey);
 
		if($this->isFile){
			$objDSig->add509Cert(file_get_contents($this->cert));
		} else {
			$objDSig->add509Cert($this->cert);
		}
 
		$objDSig->appendSignature($node, $top);
	}
}

You may notice that I am invoking a forked function called addReference2. I found that I needed to make changes to the existing like-named functions within the XMLSecurityDSig class.

private function addRefInternal2($sinfoNode, $node, $algorithm, $arTransforms=NULL, $refId, $refURI) {
 
    	$refNode = $this->createNewSignNode('Reference');
    	$sinfoNode->appendChild($refNode);
 
    	if (! $node instanceof DOMDocument) {
    		$refNode->setAttribute("URI", '#'.$refURI);
    		$refNode->setAttribute("Id", $refId);
    	}
 
    	$transNodes = $this->createNewSignNode('Transforms');
    	$refNode->appendChild($transNodes);
 
    	if (is_array($arTransforms)) {
    		foreach ($arTransforms AS $transform) {
    			$transNode = $this->createNewSignNode('Transform');
    			$transNodes->appendChild($transNode);
    			if (is_array($transform) &&
    					(! empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116'])) &&
    					(! empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['query']))) {
    				$transNode->setAttribute('Algorithm', 'http://www.w3.org/TR/1999/REC-xpath-19991116');
    				$XPathNode = $this->createNewSignNode('XPath', $transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['query']);
    				$transNode->appendChild($XPathNode);
    				if (! empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['namespaces'])) {
    					foreach ($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['namespaces'] AS $prefix => $namespace) {
    						$XPathNode->setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:$prefix", $namespace);
    					}
    				}
    			} else {
    				$transNode->setAttribute('Algorithm', $transform);
    			}
    		}
    	} elseif (! empty($this->canonicalMethod)) {
    		$transNode = $this->createNewSignNode('Transform');
    		$transNodes->appendChild($transNode);
    		$transNode->setAttribute('Algorithm', $this->canonicalMethod);
    	}
 
    	$canonicalData = $this->processTransforms($refNode, $node);
    	$digValue = $this->calculateDigest($algorithm, $canonicalData);
 
    	$digestMethod = $this->createNewSignNode('DigestMethod');
    	$refNode->appendChild($digestMethod);
    	$digestMethod->setAttribute('Algorithm', $algorithm);
 
    	$digestValue = $this->createNewSignNode('DigestValue', $digValue);
    	$refNode->appendChild($digestValue);
    }
 
    public function addReference2($node, $algorithm, $arTransforms=NULL, $refId, $refURI) {
    	if ($xpath = $this->getXPathObj()) {
    		$query = "./secdsig:SignedInfo";
    		$nodeset = $xpath->query($query, $this->sigNode);
    		if ($sInfo = $nodeset->item(0)) {
    			$this->addRefInternal2($sInfo, $node, $algorithm, $arTransforms, $refId, $refURI);
    		}
    	}
    }

And now the helper class is called.

require("SmartCloudToken.php");
 
// get keys from plain-text
// require("./sample/SmartCloudKeys.php");
//$saml = new SmartCloudToken(SmartCloudKeys::PRIVATE_KEY, SmartCloudKeys::PUBLIC_KEY);
 
// get keys from PEM files
$saml = new SmartCloudToken("key_private.pem", "key_public.pem", TRUE);
 
// create a SAML token for the authenticated user
$token = $saml->getToken("https://apps.na.collabserv.lotus.com/sps/sp/saml11/login", "https://smartcloud.demos.ibm.com/FIM/sps/SAML/saml11", $user);

If you are wondering about the format of the PEM file, it’s the following (with some portion omitted).

-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDGs8KECW6ZnDgAyq0e0noHPdKFn7yj9WQgwJQ9SCaLGWg4ZIrw
Z+aXturG7ZF2MqDBl4ZGqBVeciEvpWu61dqDoo0dQiQgyPDVv4v2WNWZao+sbW3N
TvkQ75JxRPF/nlC/zVH2nmKwrmx426If+4pSNlqJUrpAJdA79uFQ7yR0xQIDAQAB
AoGAFv2vsRViTbXMqRLKazmRUwstM7bi3dnD5yJBRMH3a7rZ20SO6vgqrz1D9xZ/
...
DDxA5lkz9YqW7c8wvIoX3I+fHsMH/Bmc+Pn0KUH2K/uRdsltXAIiiYM/5PhnM6G/
csiByWjbj8XkNEY0LQJAauns/I5KqaNOJPzo2WSHiQ878wkjHVDp07l3piaqqjx4
uBG3Fdv4LUXD+R2/1FYO7clxkOo8wKnPEg67gKchMA==
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIDjTCCAvagAwIBAgIJAP+9n7kPnwwLMA0GCSqGSIb3DQEBBQUAMIGMMQswCQYD
VQQGEwJVUzEQMA4GA1UECBMHR2VvcmdpYTEQMA4GA1UEBxMHQXRsYW50YTEMMAoG
A1UEChMDSUJNMQ4wDAYDVQQLEwVMb3R1czEWMBQGA1UEAxMNZGVtb3MuaWJtLmNv
bTEjMCEGCSqGSIb3DQEJARYUdmFuX3N0YXViQHVzLmlibS5jb20wHhcNMTIxMjI4
...
BgkqhkiG9w0BAQUFAAOBgQBF+vKRrxU5lRYZNuxuI5R8k7NvokqfD1DukY3lr4r1
mAn5mnQbqd481hxqtkfQGToLlJH60Lcp5h17NDKN8jOL+mnQWik7EocnXrXKDNWm
3HUQSJecqlx6rACPYImq7ojslwmFS4UBbA0apX8QTBYfidMIsGtWVCvCgiHFCc6V
uQ==
-----END CERTIFICATE-----

This is a self-signed certificate for development purposes. It was created using openssl with the following command.

REM The following creates a self-signed certificate using openssl.
REM The resulting server key contains both the public and private
REM keys.

set openssl_exe=C:\Dev\eclipse-php\zend\Apache2\bin\openssl.exe
set openssl_conf=C:\Dev\eclipse-php\zend\Apache2\conf\openssl.cnf
set output_dir=C:\Dev\eclipse-php\zend\Apache2\htdocs\FIM\sps\SAML\saml11

%openssl_exe% req -config "%openssl_conf%" -new -x509 -days 1852 -nodes -sha1 -out "%output_dir%\server.pem" -keyout "%output_dir%\server.pem"

The above creates the PEM file with the contents of the public and private key. The IBM customer team recommends to send a password protected keystore that contains the certificate to be imported into SmartCloud. To do this, I created a keystore with ikeyman and then converted the PEM file into a p12 file. The resulting p12 file can be imported into a keystore using ikeyman.

REM The following exports a self-signed certificate using openssl into PKCS#8.
REM The resulting private key may be imported into a JKS keystore.

set openssl_exe=C:\Dev\eclipse-php\zend\Apache2\bin\openssl.exe
set output_dir=C:\Dev\eclipse-php\zend\Apache2\htdocs\FIM\sps\SAML\saml11

%openssl_exe% pkcs12 -export -inkey "%output_dir%\key_private.pem" -in "%output_dir%\key_public.pem" -out "%output_dir%\key_private.p12"

We must then POST the XML document to the SmartCloud server to begin the authorization process. To initiate the POST, I have added the XML document into another HTML form.

SAML FormBelow you’ll see the HTML for this login form.  Of importance is the TARGET, which is where we want the user to go after the login suceeds and the SAMLResponse, which is the base64 encoded XML document.

<html>
<head>
<script>document.cookie = "IV_JCT=%2FFIM; path=/";
</script>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>SAML POST response</title>
</head>
<body>
	<h1>SmartCloud SAML Test Form</h1>
	<h2 style="display :<?php echo $tokenStyle ?>">Impersonating <?php echo $user; ?></h2>
	<p>
 
	<form method="post"
		action="SAMLForm.php" style="display :<?php echo $userStyle ?>">
		<table>
			<tr>
				<td>Impersonate</td>
				<td><input name="User" size="80"
					value=""
					type="text" />
				</td>
			</tr>
		</table>
 
		<button type="submit">POST</button>
	</form>
 
	<form method="post"
		action="https://apps.na.collabserv.com/sps/sp/saml11/login" style="display :<?php echo $tokenStyle ?>">
		<table>
			<tr>
				<td>Target URL</td>
				<td><input name="TARGET" size="80"
					value="https://apps.na.collabserv.com/meetings/host/sametime"
					type="text" />
				</td>
			</tr>
			<tr>
				<td>Raw Token</td>
				<td><textarea rows=20 cols=80>
						<?php echo htmlentities($token) ?>
					</textarea></td>
			</tr>
			<tr>
				<td>Encoded Token</td>
				<td><textarea rows=20 cols=80 name="SAMLResponse">
						<?php echo base64_encode($token) ?>
					</textarea></td>
			</tr>
		</table>
		<button type="submit">POST</button>
	</form>
 
</html>

Upon posting the SAML assertion, SmartCloud decrypts the XML document.  If the decryption succeeds, SmartCloud sets the user’s identity based on the email address in the XML and directs the user’s browser to the TARGET location.

The SAML process described requires in-depth review to understand fully. In addition to this post, I recommend the following document, which also describes the mechanics of SAML SmartCloud Federated ID.

Happy coding.

Adding Tags to SmartCloud Files

From reading documentation it wasn’t clear to me how to do a seemingly simply task, adding tags to a file. Thankfully, the answer is simple.  Add the <atom:category term=”tag”/> node to the file’s ATOM entry (on creation).  Using the previous post on Creating a SmartCloud Files Service, I’ve updated code to add the tags.

print $service->createFileByAtomEntry($subscriberId, 'Text.txt' , 'A Simple Text File', "Hello World", "tag tag2 tag3");
 
public function createFileByAtomEntry($subscriberId, $title, $description, $content, $tag = null, $contentType = FileService::TEXT){
	$entry = $this->getEntryTemplate($title, $description, $content, $tag, $contentType);
	return $this->post('/files/basic/cmis/repository/p!' . $subscriberId . '/folderc/snx:files', $entry, FileService::ATOM_XML_ENTRY);
}
 
private function getEntryTemplate($title, $description, $content, $tag, $contentType = FileService::TEXT){
	// convert the incoming tag to the ATOM XML
	$tag = $this->getTagsTemplate($tag, FileService::ATOM);
 
	$entry = <<<XML
<atom:entry xmlns:atom="http://www.w3.org/2005/Atom"><atom:title type="text">$title</atom:title><atom:summary type="text">$description</atom:summary><atom:content type="$contentType">$content</atom:content>$tag</atom:entry>
XML;
		return $entry;
}
 
private function getTags($tag){
	if($tag != null){
		// parse on the space; this is what would occur in the UI as well
		// tags can not contain spaces
		return explode(" ", $tag);
	} else {
		return array();
	}
}
 
private function getTagsTemplate($tag, $namespace = null){
	// appears that the namespace is needed for the entry template
	if($namespace != null){
		$namespace .= ':';
	} else {
		// but it should be omitted from an update (metadata) template
		$namespace = '';
	}
 
	// if a tag exists, convert the tag value to an ATOM node for the entry
	$tags = $this->getTags($tag);
 
	if(count($tags) > 0){
 
		foreach ($tags as $t){
			$tagXml.= "<". $namespace . "category term=\"" . $t . "\"/>";
		}
 
	} else {
		// no tag, so the ATOM node is empty
		$tagXml = "";
	}
 
	return $tagXml;
}

The next immediate question is, “What about updating?”  For example, how can you add and remove tags from a file.  This took some experimentation.  The solution is to issue a PUT on the File.  This was originally handled in the editMetadata function, which updates the filename and description.  The function was extended to support updating tags.  The non-obvious requirement is to remove the atom identifier.  So <atom:category term=”tag”/> in the entry template becomes simply <category term=”tag”/> in the metadata template.

print $service->editFileMetadata($subscriberId, $fileId, 'Text.txt (PROCESSED)', 'Text.txt has been successfully processed', 'tag tag4 tag5');
 
public function editFileMetadata($subscriberId, $fileId, $title, $desription, $tag = null){
	$entry = $this->getMetadataTemplate($fileId, $title, $desription, $tag);
	return $this->put('/files/basic/cmis/repository/p!' . $subscriberId . '/object/snx:file!' . $fileId, $entry, FileService::ATOM_XML);
}
 
private function getMetadataTemplate($fileId, $title, $description, $tag){
	// convert the incoming tag to the ATOM XML
	$tag = $this->getTagsTemplate($tag);
 
	$entry = <<<XML
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:cmisra="http://docs.oasis-open.org/ns/cmis/restatom/200908/"><title type="text">$title</title><summary type="text">$description</summary><cmisra:object xmlns:cmis="http://docs.oasis-open.org/ns/cmis/core/200908/"><cmis:properties><cmis:propertyId propertyDefinitionId="cmis:objectTypeId" localName="cmis_objectTypeId" displayName="Object Type Id" queryName="cmis:objectTypeId"><cmis:value>snx:file</cmis:value></cmis:propertyId><cmis:propertyString propertyDefinitionId="cmis:objectId"><cmis:value>snx:file!$fileId</cmis:value></cmis:propertyString><cmis:propertyString propertyDefinitionId="cmis:contentStreamFileName" localName="cmis_contentStreamFileName" displayName="Filename" queryName="cmis:contentStreamFileName"><cmis:value>$title</cmis:value></cmis:propertyString></cmis:properties></cmisra:object>$tag</entry>
XML;
	return $entry;
}