Tuesday, April 22, 2014

SOAP Authentication to CRM On Premise (ADFS) using JavaScript

In a previous post I showed how to authenticate to CRM Online using JavaScript. This time I'm going to show how it can work when connecting to an On Premise organization that is configured with IFD using ADFS. It should also be noted that this code might not handle ADFS configuration beyond the default settings. I'm certainly not an expert in that area, but I'm under the impression that there could be some variations in how the tokens are handled based on he ADFS setup.

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). 

In this sample, Xrm.CRMAuth.GetHeaderOnPremise 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 On Premise SOAP header & expiration.</summary>
/// <param name="url" type="String">The Url of the CRM On Premise (IFD) organization (https://org.domain.com).</param>
/// <param name="domain" type="String">Domain name of a vaid CRM user.</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.GetHeaderOnPremise = function (url, domain, username, password) {
    if (!url.match(/\/$/)) url += "/";
    username = domain + "\\" + username;
    var adfsUrl = Xrm.CRMAuth.GetADFS(url);
    var now = new Date();
    var urnAddress = url + 'XRMServices/2011/Organization.svc';
    var usernamemixed = adfsUrl + '/13/usernamemixed';
    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('<s:Header>');
    xml.push('<a:Action s:mustUnderstand="1">http://docs.oasis-open.org/ws-sx/ws-trust/200512/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('<Security s:mustUnderstand="1" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">');
    xml.push('<u:Timestamp  u:Id="' + Xrm.CRMAuth.CreateGuid() + '">');
    xml.push('<u:Created>' + now.toISOString() + '</u:Created>');
    xml.push('<u:Expires>' + new Date(now.setMinutes(now.getMinutes() + 5)).toISOString() + '</u:Expires>');
    xml.push('</u:Timestamp>');
    xml.push('<UsernameToken u:Id="' + Xrm.CRMAuth.CreateGuid() + '">');
    xml.push('<Username>' + username + '</Username>');
    xml.push('<Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">' + password + '</Password>');
    xml.push('</UsernameToken>');
    xml.push('</Security>');
    xml.push('<a:To s:mustUnderstand="1">' + usernamemixed + '</a:To>');
    xml.push('</s:Header>');
    xml.push('<s:Body>');
    xml.push('<trust:RequestSecurityToken xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">');
    xml.push('<wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">');
    xml.push('<a:EndpointReference>');
    xml.push('<a:Address>' + urnAddress + '</a:Address>');
    xml.push('</a:EndpointReference>');
    xml.push('</wsp:AppliesTo>');
    xml.push('<trust:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</trust:RequestType>');
    xml.push('</trust:RequestSecurityToken>');
    xml.push('</s:Body>');
    xml.push('</s:Envelope>\n');

    var authentication = {};
    var request = xml.join("");
    var req = new XMLHttpRequest();
    req.open("POST", usernamemixed, 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 token0 = $(req.response).find("e\\:CipherValue");
                var token1 = $(req.response).find("xenc\\:CipherValue");
                var keyIdentifer = $(req.response).find("o\\:KeyIdentifier");
                var issuerNameX509 = $(req.response).find("X509IssuerName");
                var serialNumberX509 = $(req.response).find("X509SerialNumber");
                var serverSecret = $($(req.response).find("trust\\:BinarySecret")[0]).text();
                var created = new Date(now.setMinutes(now.getMinutes() - 1)).toISOString();
                var expires = new Date(now.setMinutes(now.getMinutes() + 60)).toISOString();
                var timestamp = '<u:Timestamp xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" u:Id="_0"><u:Created>' + created + '</u:Created><u:Expires>' + expires + '</u:Expires></u:Timestamp>';
                var hashObj = new jsSHA(timestamp, "TEXT");
                var digestValue = hashObj.getHash("SHA-1", "B64", 1);
                var signedInfo = '<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"><CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></CanonicalizationMethod><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1"></SignatureMethod><Reference URI="#_0"><Transforms><Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></Transform></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></DigestMethod><DigestValue>' + digestValue + '</DigestValue></Reference></SignedInfo>';
                var b64SignedInfo = Base64.encode(signedInfo);
                var shaObj = new jsSHA(b64SignedInfo, "B64");
                var signatureValue = shaObj.getHMAC(serverSecret, "B64", "SHA-1", "B64");
                authentication.TokenExpires = $(req.response).find("wsu\\:Expires:first").text();
                authentication.Header = Xrm.CRMAuth.CreateSOAPHeaderOnPremise(url, $(keyIdentifer[0]).text(), $(token0[0]).text(), $(token1[0]).text(), $(issuerNameX509[0]).text(), $(serialNumberX509[0]).text(), signatureValue, digestValue, created, expires);
            }
        }
    };
    req.send(request);
    return authentication;
};

/// <summary>Gets the name of the ADFS server CRM uses for authentication.</summary>
/// <param name="url" type="String">The Url of the CRM On Premise (IFD) organization (https://org.domain.com).</param>
/// <returns type="String">The ADFS server url.</returns>
Xrm.CRMAuth.GetADFS = function (url) {
    var adfsUrl = null;
    var req = new XMLHttpRequest();
    req.open("GET", url + "/XrmServices/2011/Organization.svc?wsdl=wsdl0", 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) {
                adfsUrl = $(req.response).find("ms-xrm\\:Identifier");
            }
        }
    };
    req.send();
    return $(adfsUrl[0]).text().replace("http://", "https://");
};

/// <summary>Gets a CRM On Premise (IFD) SOAP header.</summary>
/// <param name="url" type="String">The Url of the CRM On Premise (IFD) organization (https://org.domain.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>
/// <param name="issuerNameX509" type="String">The certificate issuer.</param>
/// <param name="serialNumberX509" type="String">The certificate serial number.</param>
/// <param name="signatureValue" type="String">The hashsed value of the header signature.</param>
/// <param name="digestValue" type="String">The hashed value of the header timestamp.</param>
/// <param name="created" type="String">The header created date/time.</param>
/// <param name="expires" type="String">The header expiration date/tim.</param>
/// <returns type="String">The XML SOAP header to be used in future requests.</returns>
Xrm.CRMAuth.CreateSOAPHeaderOnPremise = function (url, keyIdentifer, token0, token1, issuerNameX509, serialNumberX509, signatureValue, digestValue, created, expires) {
    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('<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('<o:Security xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">');
    xml.push('<u:Timestamp xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" u:Id="_0">');
    xml.push('<u:Created>' + created + '</u:Created>');
    xml.push('<u:Expires>' + expires + '</u:Expires>');
    xml.push('</u:Timestamp>');
    xml.push('<xenc:EncryptedData Type="http://www.w3.org/2001/04/xmlenc#Element" xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">');
    xml.push('<xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes256-cbc"/>');
    xml.push('<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">');
    xml.push('<e:EncryptedKey xmlns:e="http://www.w3.org/2001/04/xmlenc#">');
    xml.push('<e:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p">');
    xml.push('<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>');
    xml.push('</e:EncryptionMethod>');
    xml.push('<KeyInfo>');
    xml.push('<o:SecurityTokenReference xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">');
    xml.push('<X509Data>');
    xml.push('<X509IssuerSerial>');
    xml.push('<X509IssuerName>' + issuerNameX509 + '</X509IssuerName>');
    xml.push('<X509SerialNumber>' + serialNumberX509 + '</X509SerialNumber>');
    xml.push('</X509IssuerSerial>');
    xml.push('</X509Data>');
    xml.push('</o:SecurityTokenReference>');
    xml.push('</KeyInfo>');
    xml.push('<e:CipherData>');
    xml.push('<e:CipherValue>' + token0 + '</e:CipherValue>');
    xml.push('</e:CipherData>');
    xml.push('</e:EncryptedKey>');
    xml.push('</KeyInfo>');
    xml.push('<xenc:CipherData>');
    xml.push('<xenc:CipherValue>' + token1 + '</xenc:CipherValue>');
    xml.push('</xenc:CipherData>');
    xml.push('</xenc:EncryptedData>');
    xml.push('<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">');
    xml.push('<SignedInfo>');
    xml.push('<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>');
    xml.push('<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1"/>');
    xml.push('<Reference URI="#_0">');
    xml.push('<Transforms>');
    xml.push('<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>');
    xml.push('</Transforms>');
    xml.push('<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>');
    xml.push('<DigestValue>' + digestValue + '</DigestValue>');
    xml.push('</Reference>');
    xml.push('</SignedInfo>');
    xml.push('<SignatureValue>' + signatureValue + '</SignatureValue>');
    xml.push('<KeyInfo>');
    xml.push('<o:SecurityTokenReference xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">');
    xml.push('<o:KeyIdentifier ValueType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID">' + keyIdentifer + '</o:KeyIdentifier>');
    xml.push('</o:SecurityTokenReference>');
    xml.push('</KeyInfo>');
    xml.push('</Signature>');
    xml.push('</o:Security>');
    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);
    });
};

Also note this code takes a dependency on jQuery as well as jsSHA (sha1.js).

So in action, it would look something like this:

var domain = "domain";
var username = "admin";
var password = "password";
var CRMSoapAuthentication = Xrm.CRMAuth.GetHeaderOnPremise(url, domain, 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);