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.

One thought on “Using SAML with SmartCloud for Social Business”

Leave a Reply

Your email address will not be published.