Friday, March 14, 2014

CRM 2011–IFD, Calling the SOAP Endpoint from Another Application Using JavaScript with Single Sign On

 

Recently, I was helping some developers trying to use the CRM 2011 SOAP Endpoint with JavaScript and also getting single sign on.  Both apps are in the same domain but the CRM endpoint in an IFD (External URL) requires a username and password login.

A little background, we have a large group of users who access CRM and other internal apps remotely.  Typically, users access CRM using the External URL that requires them to enter there credentials, even from a domain joined machine.  The application that is calling CRM, is using integrated authentication so they are not directly capturing the users credentials. 

There are a few things that need to be in place to get this to work.  First, DirectAccess.  DirectAccess is like an always connected VPN and is awesome, more companies should consider using it especially with a large remote user base.  More info on DirectAccess here http://technet.microsoft.com/en-us/network/dd420463.aspx.  Publishing CRM’s internal URL via DirectAccess is key to getting SSO to work.  After the URL is published, remote users can browse to CRM’s internal URL and Single Sign on will work.  As a side note, you must make sure your ADFS URL is also published over DirectAccess or be externally available.  It usually is if you are using ADFS / Claims but it should be checked.  DirectAccess is not required if your users can access your internal CRM URL otherwise (always in the corporate network).

Next up the code approach.  This is a rather crude example but it works and can be modified to be much more robust.   When we make a call to the web service the first time, the response will be the ADFS login page.  Usually, when you browse to CRM’s internal URL, this page is submitted automatically.  See below response script.

<script language="javascript">window.setTimeout('document.forms[0].submit()', 0);</script></body></html>



So how do we submit the form to get the MSISAuth and MSISAuth1 cookies? The answer is an iFrame. 


First, we add a placeholder <div> and an Iframe to the page that needs to call CRM’s web service. (A more robust solution would create the iFrame on the fly and keep it hidden.  For simplicity, we will leave it on the form.

<div id="placeHolder"></div>
<iframe name="iframe1"></iframe>



When we get the ADFS login page back in the response, we set the contents of the placeholder div = to the response form.  We then grab the form for the document model, change its target to the iFrame, and submit the form.  This allows use to get the MSISAuth and MSISAuth1 cookies.  Our next call to the web service will return the appropriate response.  In this example, we are just retrieving the entity metadata and displaying the SOAP response on the form.


Full Example HTML Page -

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>CRM Test</title>
</head>
<body>
<div id="placeHolder"></div>
<iframe name="iframe1"></iframe>
<script type="text/javascript">

function GetCRMToken()
{

var request = [
"<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\">",
"<soapenv:Body>",
"<Execute xmlns=\"http://schemas.microsoft.com/xrm/2011/Contracts/Services\" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\">",
"<request i:type=\"a:RetrieveAllEntitiesRequest\" xmlns:a=\"http://schemas.microsoft.com/xrm/2011/Contracts\">",
"<a:Parameters xmlns:b=\"http://schemas.datacontract.org/2004/07/System.Collections.Generic\">",
"<a:KeyValuePairOfstringanyType>",
"<b:key>EntityFilters</b:key>",
"<b:value i:type=\"c:EntityFilters\" xmlns:c=\"http://schemas.microsoft.com/xrm/2011/Metadata\">Entity</b:value>",
"</a:KeyValuePairOfstringanyType>",
"<a:KeyValuePairOfstringanyType>",
"<b:key>RetrieveAsIfPublished</b:key>",
"<b:value i:type=\"c:boolean\" xmlns:c=\"http://www.w3.org/2001/XMLSchema\">true</b:value>",
"</a:KeyValuePairOfstringanyType>",
"</a:Parameters>",
"<a:RequestId i:nil=\"true\" />",
"<a:RequestName>RetrieveAllEntities</a:RequestName>",
"</request>",
"</Execute>",
"</soapenv:Body>",
"</soapenv:Envelope>"].join("");
var req = new XMLHttpRequest();
req.open("POST", "{INTERNAL_CRM_URL_GOES_HERE}/XRMServices/2011/Organization.svc/web", false);
try { req.responseType = 'msxml-document' } catch (e) { }
req.setRequestHeader("Accept", "application/xml, text/xml, */*");
req.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
req.withCredentials = true;
req.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/xrm/2011/Contracts/Services/IOrganizationService/Execute");
req.onreadystatechange = function ()
{
if (req.readyState == 4 /* complete */)
{
req.onreadystatechange = null; //Addresses potential memory leak issue with IE
if (req.status == 200)
{
//Success
var doc = req.responseXML;
debugger;
//Submit the initial request to the iframe
var placeHolder = document.getElementById('placeHolder');
placeHolder.innerHTML = req.responseText;
document.forms[0].target = "iframe1";
document.forms[0].submit();


}
else
{
errorCallBack(_getError(req));
}
}
};
req.send(request);
}

function GetMetaData()
{

var request2 = [
"<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\">",
"<soapenv:Body>",
"<Execute xmlns=\"http://schemas.microsoft.com/xrm/2011/Contracts/Services\" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\">",
"<request i:type=\"a:RetrieveAllEntitiesRequest\" xmlns:a=\"http://schemas.microsoft.com/xrm/2011/Contracts\">",
"<a:Parameters xmlns:b=\"http://schemas.datacontract.org/2004/07/System.Collections.Generic\">",
"<a:KeyValuePairOfstringanyType>",
"<b:key>EntityFilters</b:key>",
"<b:value i:type=\"c:EntityFilters\" xmlns:c=\"http://schemas.microsoft.com/xrm/2011/Metadata\">Entity</b:value>",
"</a:KeyValuePairOfstringanyType>",
"<a:KeyValuePairOfstringanyType>",
"<b:key>RetrieveAsIfPublished</b:key>",
"<b:value i:type=\"c:boolean\" xmlns:c=\"http://www.w3.org/2001/XMLSchema\">true</b:value>",
"</a:KeyValuePairOfstringanyType>",
"</a:Parameters>",
"<a:RequestId i:nil=\"true\" />",
"<a:RequestName>RetrieveAllEntities</a:RequestName>",
"</request>",
"</Execute>",
"</soapenv:Body>",
"</soapenv:Envelope>"].join("");
var req2 = new XMLHttpRequest();

req2.open("POST", "{INTERNAL_CRM_URL_GOES_HERE}/XRMUAT/XRMServices/2011/Organization.svc/web", false);
try { req.responseType = 'msxml-document' } catch (e) { }
req2.setRequestHeader("Accept", "application/xml, text/xml, */*");
req2.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
req2.withCredentials = true;
req2.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/xrm/2011/Contracts/Services/IOrganizationService/Execute");
req2.onreadystatechange = function ()
{
if (req2.readyState == 4 /* complete */)
{
req2.onreadystatechange = null; //Addresses potential memory leak issue with IE
if (req2.status == 200)
{
//debugger;
var placeHolder2 = document.getElementById('placeHolder2');
placeHolder2.innerText = req2.responseText;
}
else
{
errorCallBack(_getError(req));
}
}
};
req2.send(request2);

}

function _getError(resp)
{
///<summary>
/// Private function that attempts to parse errors related to connectivity or WCF faults.
///</summary>
///<param name="resp" type="XMLHttpRequest">
/// The XMLHttpRequest representing failed response.
///</param>

//Error descriptions come from http://support.microsoft.com/kb/193625
if (resp.status == 12029)
{ return new Error("The attempt to connect to the server failed."); }
if (resp.status == 12007)
{ return new Error("The server name could not be resolved."); }
var faultXml = resp.responseXML;
var errorMessage = "Unknown (unable to parse the fault)";
if (typeof faultXml == "object")
{

var faultstring = null;
var ErrorCode = null;

var bodyNode = faultXml.firstChild.firstChild;

//Retrieve the fault node
for (var i = 0; i < bodyNode.childNodes.length; i++)
{
var node = bodyNode.childNodes[i];

//NOTE: This comparison does not handle the case where the XML namespace changes
if ("s:Fault" == node.nodeName)
{
for (var j = 0; j < node.childNodes.length; j++)
{
var testNode = node.childNodes[j];
if ("faultstring" == testNode.nodeName)
{
faultstring = _getNodeText(testNode);
}
if ("detail" == testNode.nodeName)
{
for (var k = 0; k < testNode.childNodes.length; k++)
{
var orgServiceFault = testNode.childNodes[k];
if ("OrganizationServiceFault" == orgServiceFault.nodeName)
{
for (var l = 0; l < orgServiceFault.childNodes.length; l++)
{
var ErrorCodeNode = orgServiceFault.childNodes[l];
if ("ErrorCode" == ErrorCodeNode.nodeName)
{
ErrorCode = _getNodeText(ErrorCodeNode);
break;
}
}
}
}

}
}
break;
}

}
}
if (ErrorCode != null && faultstring != null)
{
errorMessage = "Error Code:" + ErrorCode + " Message: " + faultstring;
}
else
{
if (faultstring != null)
{
errorMessage = faultstring;
}
}
return new Error(errorMessage);
};

//Get the MSISAuth cookies
//Ideally, the code would check for the presence of the auth cookies before making this call. If they already exist, skip this step
GetCRMToken();

//Set the timeout to make sure the ADFS post occurs before making the next call
setTimeout(GetMetaData, 400, null);


</script>

<div id="placeHolder2"></div>
</body>
</html>



You can also download the sample from here https://onedrive.live.com/redir?resid=FFCD1C6BBDDCB813!2669&authkey=!rWUBUZ81Dc0%24&ithint=file%2c.zip 


There are obviously a lot of changes that can be made to make this more robust but this should be enough to get started.


Below is the metadata SOAP response after the ADFS form is submitted.


image


This is my first post in a long time and there will be more to come.


Happy Coding!

No comments: