Monday, April 21, 2014

SOAP Authentication to CRM Online using JavaScript

The predominant use of JavaScript with Dynamics CRM for most is to extend the capabilities of the native forms, things like hiding and showing fields or making simple calculations. But the fact is Dynamics CRM is built on a service-oriented architecture, roughly translated means that just about any action you can take in the UI can be translated back to one or more SOAP requests. These SOAP requests are really nothing more than XML that is being sent to the CRM server to perform actions like retrieving records or qualifying a lead.

FYI - this is probably only useful in a limited set of circumstances like a Windows 8 application using JavaScript or something that resides in the same domain as a browser handles executing the request a bit differently when the request is made between 2 different domains (CORS). 

Those who use JavaScript with Dynamics CRM for more than the basic tasks probably already know that you can determine exactly what makes up the required XML for a given SOAP request in a few different ways. You can attach Fiddler to your CRM session, perform the action you wish to replicate, and then inspect the request sent to the server and pull out the XML. This works OK especially if you aren't a .NET developer and also gives you the benefit of seeing the type of request (GET, POST, etc...) and other header information that normally isn't directly included in the XML. For those who do have access to Visual Studio, you can download the latest version the Dynamics CRM SDK (2011 2013) and in the sample code folder under the C# and client folders there is a project called SOAPLogger. Basically what this does is allows you to write requests using C# and reference any of the messages the SDK provides and it will spit out a text file containing the XML for the request and response you've once you've executed the code. Lastly you can also check out fellow CRM MVP Jamie Miley's blog as he has already done the work of executing most of the SDK messages and posting the XML.

Once you have the proper XML to construct your request you're in good shape. The only problem is most examples you'll find are being executed from within the context CRM, like in a JavaScript or HTML web resource. At this point you are already authenticated so CRM doesn't make you submit credentials again. This brings us to the point. How do we supply credentials with our request if we want to access CRM from outside CRM? At a very high level we send the username and password to the server and in return get back a set of tokens we need to send with the header of a future request to prove that we have in fact authenticated. Also keep in mind, these tokens are only valid for a finite amount of time so you will need to keep track of that. 

So here is the sample, Xrm.CRMAuth.GetHeaderOnline returns an object that contains the header which can be injected into a SOAP request as well as the expiration date of the header so you can check that date/time versus the current date/time to determine if it has expired or not.

Xrm = window.Xrm || { __namespace: true };
Xrm.CRMAuth = Xrm.CRMAuth || { __namespace: true };

/// <summary>Gets a CRM Online SOAP header & expiration.</summary>
/// <param name="url" type="String">The Url of the CRM Online organization (https://org.crm.dynamics.com).</param>
/// <param name="username" type="String">Username of a valid CRM user.</param>
/// <param name="password" type="String">Password of a valid CRM user.</param>
/// <returns type="Object">An object containing the SOAP header and expiration date/time of the header.</returns>
Xrm.CRMAuth.GetHeaderOnline = function (url, username, password) {
    if (!url.match(/\/$/)) url += "/";
    var urnAddress = Xrm.CRMAuth.GetUrnOnline(url);
    var now = new Date();
    var xml = [];
    xml.push('<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">');
    xml.push('<s:Header>');
    xml.push('<a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action>');
    xml.push('<a:MessageID>urn:uuid:' + Xrm.CRMAuth.CreateGuid() + '</a:MessageID>');
    xml.push('<a:ReplyTo>');
    xml.push('<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>');
    xml.push('</a:ReplyTo>');
    xml.push('<a:To s:mustUnderstand="1">https://login.microsoftonline.com/RST2.srf</a:To>');
    xml.push('<o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">');
    xml.push('<u:Timestamp u:Id="_0">');
    xml.push('<u:Created>' + now.toISOString() + '</u:Created>');
    xml.push('<u:Expires>' + new Date(now.setMinutes(now.getMinutes() + 60)).toISOString() + '</u:Expires>');
    xml.push('</u:Timestamp>');
    xml.push('<o:UsernameToken u:Id="uuid-' + Xrm.CRMAuth.CreateGuid() + '-1">');
    xml.push('<o:Username>' + username + '</o:Username>');
    xml.push('<o:Password>' + password + '</o:Password>');
    xml.push('</o:UsernameToken>');
    xml.push('</o:Security>');
    xml.push('</s:Header>');
    xml.push('<s:Body>');
    xml.push('<trust:RequestSecurityToken xmlns:trust="http://schemas.xmlsoap.org/ws/2005/02/trust">');
    xml.push('<wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">');
    xml.push('<a:EndpointReference>');
    xml.push('<a:Address>urn:' + urnAddress + '</a:Address>');
    xml.push('</a:EndpointReference>');
    xml.push('</wsp:AppliesTo>');
    xml.push('<trust:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</trust:RequestType>');
    xml.push('</trust:RequestSecurityToken>');
    xml.push('</s:Body>');
    xml.push('</s:Envelope>');

    var authentication = {};
    var request = xml.join("");
    var req = new XMLHttpRequest();
    req.open("POST", "https://login.microsoftonline.com/RST2.srf", false);
    req.setRequestHeader("Connection", "Keep-Alive");
    req.setRequestHeader("Content-Type", "application/soap+xml; charset=UTF-8");
    req.onreadystatechange = function () {
        if (req.readyState === 4) {
            if (req.status === 200) {
                var token = $(req.response).find("CipherValue");
                var keyIdentifer = $(req.response).find("wsse\\:KeyIdentifier:first");
                authentication.TokenExpires = $(req.response).find("wsu\\:Expires:first").text();
                authentication.Header = Xrm.CRMAuth.CreateSOAPHeaderOnline(url, $(keyIdentifer).text(), $(token[0]).text(), $(token[1]).text());
            }
        }
    };
    req.send(request);
    return authentication;
};

/// <summary>Gets a CRM Online SOAP header.</summary>
/// <param name="url" type="String">The Url of the CRM Online organization (https://org.crm.dynamics.com).</param>
/// <param name="keyIdentifer" type="String">The KeyIdentifier from the initial request..</param>
/// <param name="token0" type="String">The first token from the initial request.</param>
/// <param name="token1" type="String">The second token from the initial request.</param>
/// <returns type="String">The XML SOAP header to be used in future requests.</returns>
Xrm.CRMAuth.CreateSOAPHeaderOnline = function (url, keyIdentifer, token0, token1) {
    var xml = [];
    xml.push('<s:Header>');
    xml.push('<a:Action s:mustUnderstand="1">http://schemas.microsoft.com/xrm/2011/Contracts/Services/IOrganizationService/Execute</a:Action>');
    xml.push('<Security xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">');
    xml.push('<EncryptedData Id="Assertion0" Type="http://www.w3.org/2001/04/xmlenc#Element" xmlns="http://www.w3.org/2001/04/xmlenc#">');
    xml.push('<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc"/>');
    xml.push('<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">');
    xml.push('<EncryptedKey>');
    xml.push('<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"/>');
    xml.push('<ds:KeyInfo Id="keyinfo">');
    xml.push('<wsse:SecurityTokenReference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">');
    xml.push('<wsse:KeyIdentifier EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509SubjectKeyIdentifier">' + keyIdentifer + '</wsse:KeyIdentifier>');
    xml.push('</wsse:SecurityTokenReference>');
    xml.push('</ds:KeyInfo>');
    xml.push('<CipherData>');
    xml.push('<CipherValue>' + token0 + '</CipherValue>');
    xml.push('</CipherData>');
    xml.push('</EncryptedKey>');
    xml.push('</ds:KeyInfo>');
    xml.push('<CipherData>');
    xml.push('<CipherValue>' + token1 + '</CipherValue>');
    xml.push('</CipherData>');
    xml.push('</EncryptedData>');
    xml.push('</Security>');
    xml.push('<a:MessageID>urn:uuid:' + Xrm.CRMAuth.CreateGuid() + '</a:MessageID>');
    xml.push('<a:ReplyTo>');
    xml.push('<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>');
    xml.push('</a:ReplyTo>');
    xml.push('<a:To s:mustUnderstand="1">' + url + 'XRMServices/2011/Organization.svc</a:To>');
    xml.push('</s:Header>');
    return xml.join("");
};

/// <summary>Creates a GUID.</summary>
/// <returns type="String">GUID.</returns>
Xrm.CRMAuth.CreateGuid = function () {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
        var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
    });
};

/// <summary>Gets the correct URN Address based on the Online region.</summary>
/// <param name="url" type="String">The Url of the CRM Online organization (https://org.crm.dynamics.com).</param>
/// <returns type="String">URN Address.</returns>
Xrm.CRMAuth.GetUrnOnline = function (url) {
    if (url.toUpperCase().indexOf("CRM4.DYNAMICS.COM") !== -1) {
        return "crmemea:dynamics.com";
    }
    if (url.toUpperCase().indexOf("CRM5.DYNAMICS.COM") !== -1) {
        return "crmapac:dynamics.com";
    }
    return "crmna:dynamics.com";
};

Also note this code takes a dependency on jQuery.

So in action, it would look something like this:

var url = "https://org.crm.dynamics.com/";
var username = "admin";
var password = "password";
var CRMSoapAuthentication = Xrm.CRMAuth.GetHeaderOnline(url, username, password);

var body = [];
body.push('<s:Body>');
body.push('<Execute xmlns="http://schemas.microsoft.com/xrm/2011/Contracts/Services">');
body.push('    <request i:type="c:WhoAmIRequest" xmlns:b="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:c="http://schemas.microsoft.com/crm/2011/Contracts">');
body.push('        <b:Parameters xmlns:d="http://schemas.datacontract.org/2004/07/System.Collections.Generic"/>');
body.push('        <b:RequestId i:nil="true"/>');
body.push('        <b:RequestName>WhoAmI</b:RequestName>');
body.push('    </request>');
body.push('</Execute>');
body.push('</s:Body>');
var xml = [];
xml.push('<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing">');
xml.push(CRMSoapAuthentication.Header);
xml.push(body.join(""));
xml.push('</s:Envelope>');
var request = xml.join("");

var req = new XMLHttpRequest();
req.open("POST", url + "XRMServices/2011/Organization.svc", true);
req.setRequestHeader("Content-Type", "application/soap+xml; charset=utf-8");
req.onreadystatechange = function () {
    if (req.readyState === 4) {
        if (req.status === 200) {
            //Handle the response
        }
    } else {
        //Error
    }
};
req.send(request);
Keep in mind this only works for CRM Online using Office 365 authentication (not Windows Live ID). On Premise using ADFS is a little different and I will cover that in a follow up blog post.