Tuesday 16 July 2013

Dynamics CRM 2011 - PHP and SOAP using Office 365

NOTE: If you are using this code I found a small issue!!
Microsoft appears to have changed the SSL version they are using. Due to this the following function needs to be modified!
 public static function GetSOAPResponse($url, $request) {
    // Set up headers.
    $headers = array(
      "POST " . "/Organization.svc" . " HTTP/1.1",
      "Host: yourorganisation.api.crm5.dynamics.com",
      'Connection: Keep-Alive',
      "Content-type: application/soap+xml; charset=UTF-8",
      "Content-length: " . strlen($request),
    );

    $cURLHandle = curl_init();
    curl_setopt($cURLHandle, CURLOPT_URL, $url);
    curl_setopt($cURLHandle, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($cURLHandle, CURLOPT_TIMEOUT, 60);
    curl_setopt($cURLHandle, CURLOPT_SSL_VERIFYPEER, FALSE);
    curl_setopt($cURLHandle, CURLOPT_FOLLOWLOCATION, TRUE);
    //COMMENT OUT THIS LINE!!curl_setopt($cURLHandle, CURLOPT_SSLVERSION, 3);
    curl_setopt($cURLHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
    curl_setopt($cURLHandle, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($cURLHandle, CURLOPT_POST, 1);
    curl_setopt($cURLHandle, CURLOPT_POSTFIELDS, $request);
    //COMMENT OUT THIS LINE!!$response = curl_exec($cURLHandle);
    // ADD THE FOLLOWING FOUR LINES INSTEAD (and a try/catch wouldn't hurt!)
    if( ! $response = curl_exec($cURLHandle)) 
    { 
        trigger_error(curl_error($cURLHandle)); 
    }
    curl_close($cURLHandle);

    return $response;
  }


Now onto the post!!
I recently found myself in a position where I needed to connect to CRM Online from PHP.  As I didn't have anywhere to put my code except for the client side creating my own wrapper for the CRM Web services didn't seem like a great option.

I found a few examples of CRM, PHP, and SOAP however many seemed to have some issue along the way (In particular around Office 365 authentication).

So firstly we will look at Office 365 authentication. To connect you need to request so tokens from the CRM Web Service as follows:

class Authentication
{
 public $username;
 public $password;
 public $keyIdentifier;
    public $securityToken0;
    public $securityToken1;

 public function Authentication($_username, $_password)
 {
  $this->username = $_username;
  $this->password = $_password;
  $this->AuthenticateUser();
 }
 
 function BuildOCPSoap()
 {
  /*
  Select the right region for your CRM
  crmna:dynamics.com - North America
  crmemea:dynamics.com - Europe, the Middle East and Africa
  crmapac:dynamics.com - Asia Pacific
  */
  $region = 'crmapac:dynamics.com';

  $OCPRequest = '<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">
       <s:Header>
      <a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action>
      <a:MessageID>urn:uuid:%s</a:MessageID>
      <a:ReplyTo>
        <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
      </a:ReplyTo>
      <a:To s:mustUnderstand="1">%s</a:To>
      <o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
        <u:Timestamp u:Id="_0">
       <u:Created>%sZ</u:Created>
       <u:Expires>%sZ</u:Expires>
        </u:Timestamp>
        <o:UsernameToken u:Id="uuid-cdb639e6-f9b0-4c01-b454-0fe244de73af-1">
       <o:Username>%s</o:Username>
       <o:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">%s</o:Password>
        </o:UsernameToken>
      </o:Security>
       </s:Header>
       <s:Body>
      <t:RequestSecurityToken xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust">
        <wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
       <a:EndpointReference>
         <a:Address>'. $region .'</a:Address>
       </a:EndpointReference>
        </wsp:AppliesTo>
        <t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>
      </t:RequestSecurityToken>
       </s:Body>
     </s:Envelope>';
  
  $OCPRequest = sprintf($OCPRequest, gen_uuid(), 'https://login.microsoftonline.com/RST2.srf',  getCurrentTime(), getNextDayTime(), $this->username, $this->password);
  return $OCPRequest;
 }
 
 public static function GetSOAPResponse($url, $request) {
    // Set up headers.
    $headers = array(
      "POST " . "/Organization.svc" . " HTTP/1.1",
      "Host: yourorganisation.api.crm5.dynamics.com",
      'Connection: Keep-Alive',
      "Content-type: application/soap+xml; charset=UTF-8",
      "Content-length: " . strlen($request),
    );

    $cURLHandle = curl_init();
    curl_setopt($cURLHandle, CURLOPT_URL, $url);
    curl_setopt($cURLHandle, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($cURLHandle, CURLOPT_TIMEOUT, 60);
    curl_setopt($cURLHandle, CURLOPT_SSL_VERIFYPEER, FALSE);
    curl_setopt($cURLHandle, CURLOPT_FOLLOWLOCATION, TRUE);
    curl_setopt($cURLHandle, CURLOPT_SSLVERSION, 3);
    curl_setopt($cURLHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
    curl_setopt($cURLHandle, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($cURLHandle, CURLOPT_POST, 1);
    curl_setopt($cURLHandle, CURLOPT_POSTFIELDS, $request);
    $response = curl_exec($cURLHandle);
    curl_close($cURLHandle);

    return $response;
  }
  
  public function AuthenticateUser()
 {
  $SOAPresult = GetSOAPResponse('https://login.microsoftonline.com/RST2.srf', $this->BuildOCPSoap());
  $responsedom = new DomDocument();
        $responsedom->loadXML($SOAPresult);
  
        $cipherValues = $responsedom->getElementsbyTagName("CipherValue");
       
        if( isset ($cipherValues) && $cipherValues->length>0){
            $this->securityToken0 =  $cipherValues->item(0)->textContent;
   $this->securityToken1 =  $cipherValues->item(1)->textContent;
   $this->keyIdentifier = $responsedom->getElementsbyTagName("KeyIdentifier")->item(0)->textContent;
        }else{
            return null;
        }
 }
      


gen_uuid returns a random guid, getCurrentTime gets the current date/time, getNextDayTime gets tomorrow (or any expiry date/time you wish)

We then send this to the authentication srf to get the required tokens

You should now have  the required tokens which you then place in your header for any requests as shown below:


public static function getHeader($_action, $_authentication) {

  $header = '
  <s:Header>
   <a:Action s:mustUnderstand="1">http://schemas.microsoft.com/xrm/2011/Contracts/Services/IOrganizationService/'.$_action .'</a:Action>
   <a:MessageID>
    urn:uuid:'.self::gen_uuid().'
   </a:MessageID>
   <a:ReplyTo><a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address></a:ReplyTo>
   <a:To s:mustUnderstand="1">
    https://yourorganisation.api.crm5.dynamics.com/XRMServices/2011/Organization.svc
   </a:To>
   <o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
   <u:Timestamp u:Id="_0">
    <u:Created>'.self::getCurrentTime().'Z</u:Created>
    <u:Expires>'.self::getNextDayTime().'Z</u:Expires>
   </u:Timestamp>
   <EncryptedData Id="Assertion0" Type="http://www.w3.org/2001/04/xmlenc#Element" xmlns="http://www.w3.org/2001/04/xmlenc#">
    <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc"></EncryptionMethod>
    <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
     <EncryptedKey>
      <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"></EncryptionMethod>
      <ds:KeyInfo Id="keyinfo">
       <wsse:SecurityTokenReference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
        <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">
         '.$_authentication->keyIdentifier.'
        </wsse:KeyIdentifier>
       </wsse:SecurityTokenReference>
      </ds:KeyInfo>
      <CipherData>
       <CipherValue>
        '.$_authentication->securityToken0.'
       </CipherValue>
      </CipherData>
     </EncryptedKey>
    </ds:KeyInfo>
    <CipherData>
     <CipherValue>
      '.$_authentication->securityToken1.'
     </CipherValue>
    </CipherData>
   </EncryptedData>
   </o:Security>
  </s:Header>';

  return $header;

 }

public static function WhoAmIRequest($request){

 $authentication = new Authentication('Username', 'Password');
 $xml = '
 <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">
  'getHeader('Execute', $authentication).'
  <s:Body>
   <Execute xmlns="http://schemas.microsoft.com/xrm/2011/Contracts/Services" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
    <request i:type="b:WhoAmIRequest" xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:b="http://schemas.microsoft.com/crm/2011/Contracts">
     <a:Parameters xmlns:c="http://schemas.datacontract.org/2004/07/System.Collections.Generic" />
     <a:RequestId i:nil="true" />
     <a:RequestName>WhoAmI</a:RequestName>
    </request>
   </Execute>
  </s:Body>
 </s:Envelope>'; 

 return self::GetSOAPResponse('https://yourorganisation.api.crm5.dynamics.com/XRMServices/2011/Organization.svc', $xml);
}

This code is slightly modified obviously from what I am using however I have the above working. The above is not intended to be the entire solution it is just expected that it might assist you in your own work.

1 comment: