Integrating Microsoft Dynamics CRM 2011 Online with Java and other non-.Net Clients
Author: Philippe Delle Case
Introduction
While the most common style of .Net development for Microsoft Dynamics CRM Online is using the SOAP endpoint with early or late bound types, there are situations where you need to use the Web Services Description Language (WSDL) endpoint directly. This article is intended to provide some understanding how to leverage this light-weight endpoint that can be used from non-.NET languages. It provides some practical examples in Java that could be transposed to any other popular language.
Microsoft Dynamics CRM Online organizations created since July of 2012 are using the Microsoft Office 365 authentication and billing platform. This was a switch from using Microsoft LiveID since the beginning of Microsoft CRM Online in April of 2008. This article is covering the latest Office 365 Authentication scenario that would fit most of the recent CRM online deployments. An article from the Microsoft Developer Network (referenced at the end of this article) is covering the LiveID scenario.
We'll start to look at the generation of the Microsoft Dynamics CRM Organization web service stub from its WSDL endpoint and then we'll see how to authenticate a user via the Office 365 platform's Secure Token Service (STS) and encode the resulting tokens in a security header securing all operations with the Dynamics CRM Organization service. Finally some of the basic Organization service operations will be covered along with some considerations around performances and potential issues.
Prerequisites for the Java Client
Java SE SDK 7 or later version
- <www.oracle.com/technetwork/java/javase/downloads/index.html>
- Make sure that the System variable JAVA_HOME is set and pointing to the main Java Repository and that the path to the Java binaries is appended to the Operating System Path.
The following Java libraries:
- Apache Axis2 1.6.2 or later version
- <axis.apache.org/axis2/java/core/download.cgi>
- Apache HTTP Components Client 4.2.5 or later
- <hc.apache.org/>
- Apache HTTP Components Core 4.3 or later
- <hc.apache.org/httpcomponents-core-ga/>
- Ant 1.9.2 or later version, if you wish to automate the build and eventually the web service stub generation
- <ant.apache.org/bindownload.cgi>
- Apache Axis2 1.6.2 or later version
You'll need of course a subscription or trial to Microsoft Dynamics CRM 2011 Online. If you don't have a Dynamics CRM Org. available, you can test drive it here: www.microsoft.com/en-us/dynamics/crm-test-drive.aspx.
Dynamics CRM Organization Web Service Stub
The Microsoft Dynamics CRM Organization web service stub can be generated with the WSDL2Java command, available right from the bin folder of Apache Axis2:
{Path to Axis2 Lib}\axis2-1.6.2\bin> WSDL2java -uri https ://{Your CRM Org domain}. crm.dynamics.com/XRMServices/2011/Organization.svc?wsdl -p {Java package for the generated stub source} -s -o {Path for the generated stub source} |
If you prefer to use Apache Ant in order to automate the stub generation, the Ant task would look like this in your build.xml file:
<!—Microsoft Dynamics CRM 2011 Organization Stub generation --> <target name="gen-organization-stub"> <taskdef name="axis2-wsdl2java" classname="org.apache.axis2.tool.ant.AntCodegenTask" classpathref="build.classpath" /> <axis2-wsdl2java wsdlfilename=https:// {Your CRM Org domain} .crm.dynamics.com/XRMServices/2011/Organization.svc?wsdl packageName=" {Java package for the generated stub source} " output=" {Path for the generated stub source} " syncOnly="true" /> </target> |
The URI prefix to the Dynamics CRM 2011 Organization Service should look like https://myCRMOrg.crm.dynamics.com/ if your Organization is hosted in North America, https://myCRMOrg.crm4.dynamics.com/ for EMEA and https://myCRMOrg.crm5.dynamics.com/ for APAC.
An example of Java package could be like "com.mycompany.dynamicscrm.integration" and the path for the generated stub source should be right within your Java project source folder.
Note that with the "s/syncOnly" option of the command, the generated stubs will contain exclusively the synchronous invocation methods.
A detailed documentation of the WSDL2Java command can be found here.
The generated source code for the stub should be a set of 9 Java classes:
OrganizationServiceStub.java is the main stub for the Microsoft Dynamics CRM 2011 Organization Service and the other classes are Java exceptions related to the service's basic operations.
Authentication via the Microsoft Office 365 Secure Token Service (STS)
Typical STS Authentication request
Here is a typical SOAP/HTTPS request to authenticate a user through the Microsoft Office 365 STS with the associated main headers:
- POST https://login.microsoftonline.com/RST2.srf
- HTTP/1.1
- Content-Type: application/soap+xml; charset=utf-8
- Host: login.microsoftonline.com
<s:Envelope xmlns:s= "www.w3.org/2003/05/soap-envelope" xmlns:a= "www.w3.org/2005/08/addressing" xmlns:u= "docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" > <s:Header> <a:Action s:mustUnderstand= "1" >schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action> <a:MessageID>urn:uuid: {Message ID} </a:MessageID> <a:ReplyTo><a:Address>www.w3.org/2005/08/addressing/anonymous</a:Address></a:ReplyTo> <VsDebuggerCausalityData xmlns= "schemas.microsoft.com/vstudio/diagnostics/servicemodelsink" > uIDPo2V68j15KH9PqGf9DWiAfGQAAAAA/Dr1z6qvqUGzr5Yv4aMcdIr9AKDFU7VHn7lpNp0zeXEACQAA</VsDebuggerCausalityData> <a:To s:mustUnderstand= "1" >https://login.microsoftonline.com/RST2.srf</a:To> <o:Security s:mustUnderstand= "1" xmlns:o= "docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" > <u:Timestamp u:Id= "_0" > <u:Created> {Request Timestamp} </u:Created> <u:Expires> {Request Expiry Timestamp} </u:Expires> </u:Timestamp> <o:UsernameToken u:Id= " {Token ID} " > <o:Username> {User Name} </o:Username> <o:Password Type= "docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText" > {User Password} </o:Password> </o:UsernameToken> </o:Security> </s:Header> <s:Body> <t:RequestSecurityToken xmlns:t= "schemas.xmlsoap.org/ws/2005/02/trust" > <wsp:AppliesTo xmlns:wsp= "schemas.xmlsoap.org/ws/2004/09/policy" > <a:EndpointReference> <a:Address>urn:crmna:dynamics.com</a:Address> </a:EndpointReference> </wsp:AppliesTo> <t:RequestType>schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType> </t:RequestSecurityToken> </s:Body> </s:Envelope> |
The Parameters
in red
are the dynamic information to define as variables in every authentication request:
- {Message ID} : unique request Id, e.g. '0d457c4e-8b44-4100-8b7e-085ca6303c7f',
- {Request Timestamp} : timestamp of the request (now), e.g. '2013-08-17T09:32:27.786Z',
- {Request Expiry Timestamp} : timestamp when the request should expire (could be now + 5 minutes), e.g. '2013-08-17T09:37:27.786Z',
- {Token ID} : unique token Id, e.g. 'uuid-8c6514f1-8cb5-4c6b-8c0f-e476c7fd7a90-1',
- {User Name} : User login, e.g. 'admin@MyCRMorg.onmicrosoft.com',
- {User Password} : User password.
A typical successful authentication answer looks like this:
<?xml version= "1.0" encoding= "utf-8" ?> <S:Envelope xmlns:S= "www.w3.org/2003/05/soap-envelope" xmlns:wsse= "docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsu= "docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:wsa= "www.w3.org/2005/08/addressing" > <S:Header> <wsa:Action xmlns:S= "www.w3.org/2003/05/soap-envelope" xmlns:wsa= "www.w3.org/2005/08/addressing" xmlns:wsu= "docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id= "Action" S:mustUnderstand= "1" >schemas.xmlsoap.org/ws/2005/02/trust/RSTR/Issue</wsa:Action> <wsa:To xmlns:S= "www.w3.org/2003/05/soap-envelope" xmlns:wsa= "www.w3.org/2005/08/addressing" xmlns:wsu= "docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id= "To" S:mustUnderstand= "1" >schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:To> <wsse:Security S:mustUnderstand= "1" > <wsu:Timestamp xmlns:wsu= "docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id= "TS" > <wsu:Created>2013-08-17T09:33:00Z</wsu:Created> <wsu:Expires>2013-08-17T09:38:00Z</wsu:Expires> </wsu:Timestamp> </wsse:Security> </S:Header> <S:Body> <wst:RequestSecurityTokenResponse xmlns:S= "www.w3.org/2003/05/soap-envelope" xmlns:wst= "schemas.xmlsoap.org/ws/2005/02/trust" xmlns:wsse= "docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsu= "docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:saml= "urn:oasis:names:tc:SAML:1.0:assertion" xmlns:wsp= "schemas.xmlsoap.org/ws/2004/09/policy" xmlns:psf= "schemas.microsoft.com/Passport/SoapServices/SOAPFault" > <wst:TokenType>urn:oasis:names:tc:SAML:1.0</wst:TokenType> <wsp:AppliesTo xmlns:wsa= "www.w3.org/2005/08/addressing" > <wsa:EndpointReference><wsa:Address>urn:crmna:dynamics.com</wsa:Address></wsa:EndpointReference> </wsp:AppliesTo> <wst:Lifetime> <wsu:Created>2013-08-17T09:33:00Z</wsu:Created> <wsu:Expires>2013-08-17T17:33:00Z</wsu:Expires> </wst:Lifetime> <wst:RequestedSecurityToken> <EncryptedData xmlns= "www.w3.org/2001/04/xmlenc#" Id= "Assertion0" Type= "www.w3.org/2001/04/xmlenc#Element" > <EncryptionMethod Algorithm= "www.w3.org/2001/04/xmlenc#tripledes-cbc" > </EncryptionMethod> <ds:KeyInfo xmlns:ds= "www.w3.org/2000/09/xmldsig#" > <EncryptedKey> <EncryptionMethod Algorithm= "www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p" > </EncryptionMethod> <ds:KeyInfo Id= "keyinfo" > <wsse:SecurityTokenReference> <wsse:KeyIdentifier EncodingType= "docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary" ValueType= "docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509SubjectKeyIdentifier" > D3xjUG3HGaQuKyuGdTWuf6547Lo= </wsse:KeyIdentifier> </wsse:SecurityTokenReference> </ds:KeyInfo> <CipherData> <CipherValue> {Security Token 0} </CipherValue> </CipherData> </EncryptedKey> </ds:KeyInfo> <CipherData> <CipherValue> {Security Token 1} </CipherValue> </CipherData> </EncryptedData> </wst:RequestedSecurityToken> <wst:RequestedAttachedReference> <wsse:SecurityTokenReference> <wsse:KeyIdentifier ValueType= "docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID" > {Key Identifier} </wsse:KeyIdentifier> </wsse:SecurityTokenReference> </wst:RequestedAttachedReference> <wst:RequestedProofToken> <wst:BinarySecret>/NbucQb9wbn4h2sZZDpRzwsF8q88SeyP</wst:BinarySecret> </wst:RequestedProofToken> </wst:RequestSecurityTokenResponse> </S:Body> </S:Envelope> |
We can extract from this request the following credentials: the Security Token 0, the Security Token 1 and the Key Identifier.
A typical failed authentication answer, because of invalid credentials, looks like this:
<?xml version= "1.0" encoding= "utf-8" ?> <S:Envelope xmlns:S= "www.w3.org/2003/05/soap-envelope" xmlns:wsse= "docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsu= "docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:wst= "schemas.xmlsoap.org/ws/2005/02/trust" xmlns:psf= "schemas.microsoft.com/Passport/SoapServices/SOAPFault" > <S:Body> <S:Fault> <S:Code> <S:Value>S:Sender</S:Value> <S:Subcode> <S:Value>wst:FailedAuthentication</S:Value> </S:Subcode> </S:Code> <S:Reason><S:Text xml:lang= "en-US" >Authentication Failure</S:Text></S:Reason> <S:Detail><psf:error> <psf:value>0x80048821</psf:value> <psf:internalerror> <psf:code>0x80041012</psf:code> <psf:text>The entered and stored passwords do not match. </psf:text> </psf:internalerror> </psf:error></S:Detail> </S:Fault> </S:Body> </S:Envelope> |
Implementation in Java
First create a template with the full authentication SOAP request's envelope and tokenize the dynamic variables (tokens are defined with %S):
// SOAP envelope template for MSDC Online STS authentication (O365 online platform) static public final String MSDC_ONLINE_AUTH_SOAP_ENVELOPE_TEMPLATE = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + "<s:Envelope xmlns:s=\"www.w3.org/2003/05/soap-envelope\" " + "xmlns:a=\"www.w3.org/2005/08/addressing\" " + "xmlns:u=\"docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\">" + " <s:Header>" + " <a:Action s:mustUnderstand=\"1\">schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action>" + " <a:MessageID>urn:uuid: %s</a:MessageID>" + " <a:ReplyTo><a:Address>www.w3.org/2005/08/addressing/anonymous</a:Address></a:ReplyTo>" + " <VsDebuggerCausalityData xmlns=\"schemas.microsoft.com/vstudio/diagnostics/servicemodelsink\">uIDPo2V68j15KH9PqGf9DWiAf GQAAAAA/Dr1z6qvqUGzr5Yv4aMcdIr9AKDFU7VHn7lpNp0zeXEACQAA</VsDebuggerCausalityData>" + " <a:To s:mustUnderstand=\"1\">https://login.microsoftonline.com/RST2.srf</a:To>" + " <o:Security s:mustUnderstand=\"1\" xmlns:o=\"docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\">" + " <u:Timestamp u:Id=\"_0\">" + " <u:Created> %s</u:Created>" + " <u:Expires> %s</u:Expires>" + " </u:Timestamp>" + " <o:UsernameToken u:Id=\" %s\">" + " <o:Username> %s</o:Username>" + " <o:Password Type=\"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=\"schemas.xmlsoap.org/ws/2005/02/trust\">" + " <wsp:AppliesTo xmlns:wsp=\"schemas.xmlsoap.org/ws/2004/09/policy\">" + " <a:EndpointReference>" + " <a:Address>urn:crmna:dynamics.com</a:Address>" + " </a:EndpointReference>" + " </wsp:AppliesTo>" + " <t:RequestType>schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>" + " </t:RequestSecurityToken>" + " </s:Body>" + "</s:Envelope>"; |
Generate the dynamic token values and prepare the SOAP request in order to authenticate a user through the Microsoft Online STS:
// Prepare input parameter for CRM Authentication Request // > Random Message Id String paramMessageId = UUID.randomUUID().toString(); // > Request Timestamp and +5 minutes validity TimeZone gmtTZ = TimeZone.getTimeZone("GMT"); SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); formatter.setTimeZone(gmtTZ); Calendar calendar = Calendar.getInstance(gmtTZ); Date timestampRequest = calendar.getTime(); calendar.add(Calendar.MINUTE, 5); Date timestampExpiryRequest = calendar.getTime(); String paramTimestampRequest = formatter.format(timestampRequest); String paramTimestampExpiryRequest = formatter.format(timestampExpiryRequest); // > Random Token Id String paramTokenId = "uuid-" + UUID.randomUUID().toString() + "-1";
// Prepare CRM Online authentication SOAP request String onlineCRMAuthSOAPEnvelope = String.format( MSDC_ONLINE_AUTH_SOAP_ENVELOPE_TEMPLATE, paramMessageId, paramTimestampRequest, paramTimestampExpiryRequest, paramTokenId, userName, userPassword); |
Now it is time to send the authentication request to the Microsoft Online STS, via SOAP/HTTPS …
// Send CRM Online authentication SOAP request to Microsoft online STS String onlineCRMAuthResponseXML = postSOAPRequest( "https://login.microsoftonline.com/RST2.srf", onlineCRMAuthSOAPEnvelope); |
… For that, we can leverage the Apache HTTP Components:
public static String postSOAPRequest(URI serviceUri, String soapEnvelope) throws ParseException, IOException { HttpResponse response = null; HttpParams params = new BasicHttpParams(); params.setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 180000); //time out in ms HttpClient client = new DefaultHttpClient(params); HttpPost post = new HttpPost(serviceUri); StringEntity entity = new StringEntity(soapEnvelope); post.setHeader("Content-Type", "application/soap+xml; charset=UTF-8"); post.setEntity(entity);
response = client.execute(post);
return EntityUtils.toString(response.getEntity()); } |
Finally, parse the SOAP response from the STS and gather 3 credentials: securityToken0, securityToken1
and keyIdentifier.
If the credentials cannot be found, then we assume we are facing a failed authentication answer. In that case, the reason and the detail of the error can be parsed instead:
// Parse the CRM Online authentication SOAP response from STS
// Create a Java DOM XML Parser DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = builderFactory.newDocumentBuilder(); // Parse XML with Java DOM XML Parser Document xmlDocument = builder.parse(new ByteArrayInputStream(onlineCRMAuthResponseXML.getBytes()));
// Retrieve security tokens and key identifier from security token response. XPath xpath = XPathFactory.newInstance().newXPath(); String securityToken0 = MSDCSoapHelper.readStringValFromXmlDocument(xmlDocument, "//*[local-name()='CipherValue']",xpath);
// If first token is blank, search eventual authentication failure message if((securityToken0==null)||(securityToken0.isEmpty())){ String errorReason = MSDCSoapHelper.readStringValFromXmlDocument(xmlDocument, "//*[local-name()='Reason']",xpath); String errorDetail = MSDCSoapHelper.readStringValFromXmlDocument(xmlDocument, "//*[local-name()='Detail']", xpath).substring(20);
if((errorReason!=null)&&(errorReason.equalsIgnoreCase("Authentication Failure"))){ logger.debug("Failed authentication for User '" + userName + "'. Reason is '" + errorReason + "' and Detail is " + errorDetail); throw new MSDCAuthenticationException(errorDetail); // Exception to craft according to your needs } else { logger.debug("Failed authentication for User '" + userName + "' but cannot parse the reasons"); throw new MSDCAuthenticationException ("Failed authentication for unexpected reasons); } } String securityToken1 = MSDCSoapHelper.readStringValFromXmlDocument(xmlDocument, "(//*[local-name()='CipherValue'])[2]",xpath); String keyIdentifier = MSDCSoapHelper.readStringValFromXmlDocument(xmlDocument, "//*[local-name()='KeyIdentifier']", xpath); |
Calling Dynamics CRM Organization Service Invocation Methods in Java
Instantiation of the Organization Service Stub
In order to instantiate the Organization Service Stub from the Java classes generated from the WSDL, we must first configure Axis2 by creating a configuration context from the 'axis2.xml' file that we placed at the root of the project source code. This file is a copy of the standard 'axis2.xml' file that can be found in the 'conf' directory of the Axis2 Java library.
We need to also pass, along with the configuration context, the URL of the Dynamics CRM 2011 Online Organization.
// Create OrganizationServiceStub String fileSeperator = System.getProperty("file.separator"); String userDir = System.getProperty("user.dir"); String axis2ConfigFilePath = userDir + fileSeperator + "src" + fileSeperator + "axis2.xml";
ConfigurationContext ctx = ConfigurationContextFactory.createConfigurationContextFromFileSystem(userDir, axis2ConfigFilePath); organizationServiceStub = new OrganizationServiceStub(ctx, "https://myCRMOrg.crm.dynamics.com");
// Get service client implementation used by this stub. serviceClient = organizationServiceStub._getServiceClient(); |
We keep most of the default parameters in the 'axis2.xml' file except a custom inflow predefined Phase called 'MustUnderstandChecker' that must be declared in this file:
. . . <!-- ================================================= --> <!-- Phases --> <!-- ================================================= --> <phaseOrder type= "InFlow" > . . . <phase name= "RMPhase" /> <!-- System predefined phases --> <!-- After Postdispatch phase module author or service author can add any phase he want --> <phase name= "OperationInPhase" > <handler name= "MustUnderstandChecker" class= " com.mycompany.dynamicscrm.integration.Axis2MustUnderstandChecker " > <order phase= "OperationInPhase" /> </handler> </phase> . . . </phaseOrder> . . . |
Here is an implementation of the Axis2MustUnderstandChecker class that tells Axis2 client to process the security SOAP header block from the message context header:
/*** * Handler for SOAP header. * */ public final class Axis2MustUnderstandChecker extends AbstractHandler {
public Axis2MustUnderstandChecker() { }
/* (non-Javadoc) * Process the Security SOAP header block from the message context header. * @see org.apache.axis2.engine.Handler#invoke(org.apache.axis2.context.MessageContext) */ public InvocationResponse invoke(MessageContext msgContext) throws AxisFault {
SOAPHeader header = msgContext.getEnvelope().getHeader();
if (header != null) { Iterator<?> blocks = header.examineAllHeaderBlocks();
while (blocks.hasNext()) { SOAPHeaderBlock block = (SOAPHeaderBlock) blocks.next();
if(block != null){ if (block.getLocalName().equals("Security")) { block.setProcessed(); } } } } return InvocationResponse.CONTINUE; } } |
SOAP Requests options and Security SOAP header block
After the instantiation of the Organization Service Stub, some options must be set and a fresh Security SOAP header block must be defined:
try { Options scOptions = serviceClient.getOptions(); scOptions.setMessageId("urn:uuid:" + UUID.randomUUID().toString()); EndpointReference endPoint = new EndpointReference("www.w3.org/2005/08/addressing/anonymous"); scOptions.setReplyTo(endPoint); serviceClient.setOptions(scOptions);
// Add fresh Security SOAP Header block serviceClient.addHeader(generateFreshSecuritySoapHeaderBlock(securityHeader) ); serviceClient.engageModule("addressing");
} catch (AxisFault af) { throw new MSDCIntegrationException("Unexpected web service error", af); } |
Here is the detail of the method 'generateFreshSecuritySoapHeaderBlock'
that generate a brand new security SOAP header block including some fresh timestamps with a validity period (set arbitrarily in our case to 5 minutes) and a security header embedding the credentials from the Microsoft Online STS authentication:
private SOAPHeaderBlock generateFreshSecuritySoapHeaderBlock (String securityHeaderStr) throws XMLStreamException {
SOAPHeaderBlock securitySoapHeaderBlock = null; OMFactory omFactory = OMAbstractFactory.getOMFactory(); OMNamespace securitySecextNS = omFactory.createOMNamespace("docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "o"); OMNamespace securityUtilityNS = omFactory.createOMNamespace("docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", "u");
// Create fresh Time stamp element for the SOAP header block TimeZone gmtTZ = TimeZone.getTimeZone("GMT"); SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); formatter.setTimeZone(gmtTZ); Calendar calendar = Calendar.getInstance(gmtTZ); Date timestampRequest = calendar.getTime(); calendar.add(Calendar.MINUTE, 5); Date timestampExpiryRequest = calendar.getTime(); String timestampRequestStr = formatter.format(timestampRequest); String timestampExpiryRequestStr = formatter.format(timestampExpiryRequest);
OMElement timeStampElement = omFactory.createOMElement("Timestamp", securityUtilityNS); timeStampElement.addAttribute("Id", "_0", securityUtilityNS); OMElement createdElement = omFactory.createOMElement("Created", securityUtilityNS); OMText createdTime = omFactory.createOMText(timestampRequestStr + "Z"); createdElement.addChild(createdTime); OMElement expiresElement = omFactory.createOMElement("Expires", securityUtilityNS); OMText expiresTime = omFactory.createOMText(timestampExpiryRequestStr + "Z"); expiresElement.addChild(expiresTime); timeStampElement.addChild(createdElement); timeStampElement.addChild(expiresElement);
// Create the Security SOAP header block and add, as a child, Time stamp element securitySoapHeaderBlock = OMAbstractFactory.getSOAP12Factory().createSOAPHeaderBlock("Security", securitySecextNS); securitySoapHeaderBlock.setMustUnderstand(true); securitySoapHeaderBlock.addChild(timeStampElement); securitySoapHeaderBlock.addChild(AXIOMUtil.stringToOM(omFactory, securityHeaderStr));
return securitySoapHeaderBlock; } |
The method 'generateFreshSecuritySoapHeaderBlock' takes the Security Header string as an input parameter and it can be built with the following template:
// Security header template static public final String MSDC_SECURITY_HEADER_TEMPLATE = "<EncryptedData xmlns=\"www.w3.org/2001/04/xmlenc#\" Id=\"Assertion0\" Type=\"www.w3.org/2001/04/xmlenc#Element\">" + " <EncryptionMethod Algorithm=\"www.w3.org/2001/04/xmlenc#tripledes-cbc\"/>" + " <ds:KeyInfo xmlns:ds=\"www.w3.org/2000/09/xmldsig#\">" + " <EncryptedKey>" + " <EncryptionMethod Algorithm=\"www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p\"/>" + " <ds:KeyInfo Id=\"keyinfo\">" + " <wsse:SecurityTokenReference xmlns:wsse=\"docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\">" + " <wsse:KeyIdentifier EncodingType=\"docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary\" " + " ValueType=\"docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509SubjectKeyIdentifier\"> %s</wsse:KeyIdentifier>" + " </wsse:SecurityTokenReference>" + " </ds:KeyInfo>" + " <CipherData>" + " <CipherValue> %s</CipherValue>" + " </CipherData>" + " </EncryptedKey>" + " </ds:KeyInfo>" + " <CipherData>" + " <CipherValue> %s</CipherValue>" + " </CipherData>" + "</EncryptedData>"; |
Generate the security header string with the following Java code, replacing the 3 tokens in the template by the credentials obtained from the Microsoft Online STS authentication:
// Generate security header securityHeader = String.format( MSDC_SECURITY_HEADER_TEMPLATE, keyIdentifier, securityToken0, securityToken1); |
Stub Invocation Methods
Here is an example of a high level 'Create' method that create an object in Dynamics CRM 2011 Online with the following parameters:
- entityLogicalName: the logical name of the entity to create, e.g. 'account',
- attributesHash: an HashMap of attributes (pairs of attribute keys and values), e.g. {"name", "Test Account"},{"address1_city", "Paris"}, etc.,
- organizationService: the Dynamics CRM Organization Service stub instantiated and augmented with the options and the fresh Security SOAP header block.
public String executeCreate(String entityLogicalName, HashMap<String,String> attributesHash, OrganizationServiceStub organizationService) throws MSDCIntegrationException {
// Transfer attributes from HashMap to Stub attribute collection OrganizationServiceStub.AttributeCollection attributeCollection = new OrganizationServiceStub.AttributeCollection(); Iterator<String> it = attributesHash.keySet().iterator(); while(it.hasNext()){ String key = it.next(); OrganizationServiceStub.KeyValuePairOfstringanyType KeyValuePair = new OrganizationServiceStub.KeyValuePairOfstringanyType(); KeyValuePair.setKey(key); KeyValuePair.setValue(attributesHash.get(key)); attributeCollection.addKeyValuePairOfstringanyType(KeyValuePair); }
// Create Entity with attributes OrganizationServiceStub.Entity entity = new OrganizationServiceStub.Entity(); entity.setLogicalName(entityLogicalName); entity.setAttributes(attributeCollection); OrganizationServiceStub.Create createEntity = new OrganizationServiceStub.Create(); createEntity.setEntity(entity);
// Send Create command to Organization web service String resultGuid = null; try { OrganizationServiceStub.CreateResponse createResponse; createResponse = organizationService.create(createEntity); OrganizationServiceStub.Guid createResultGuid = createResponse.getCreateResult(); resultGuid = createResultGuid.getGuid(); } catch (RemoteException | IOrganizationService_Create_OrganizationServiceFaultFault_FaultMessage e) { throw new MSDCIntegrationException("Unexpected web service error", e); }
logger.debug("Entity '" + entityLogicalName + "' created successfully with GUID = " + resultGuid); return resultGuid; } |
Here are some of the other public methods you can invoke on the Organization Service stub:
- Associate: Creates a link between records.
- Delete: Deletes a record.
- Disassociate: Deletes a link between records.
- Execute: Executes a message in the form of a request, and returns a response.
- Retrieve: Retrieves a record.
- RetrieveMultiple: Retrieves a collection of records.
- Update: Updates an existing record.
Performances consideration
Instantiating the Organization Service stub can takes up to 75% of the time of a single transaction, this is why this operation should be minimized at any cost.
It is recommend to keep the same stub instance with one security header for a few consecutive operations within a single stateless transaction. The security SOAP header block has got a timestamp with a validity period and this period should not expire.
If the application is stateful, the stub can be cached. Once instantiated, the security SOAP header block can be regenerated for each further usages. The cost of this operation is minimal.
Before to rebuild a security SOAP header block, the headers of the stub's service client must be cleared with the command 'removeHeaders()':
// Recycling organizationServiceStub serviceClient = organizationServiceStub._getServiceClient(); // Remove existing headers in order to produce a fresh one serviceClient.removeHeaders(); |
Troubleshooting
- Always make sure that the account used to authenticate on the Dynamics CRM server is valid and that it has the required privileges to execute the desired operations on the Organization service.
- Undeclared namespace prefix "wsx" Exception at Java runtime when instantiating OrganizationServiceStub:
- Exception detail: org.apache.neethi.builders.converters.ConverterException: com.ctc.wstx.exc.WstxParsingException: Undeclared namespace prefix "wsx"
- Probable cause: the generated stub is declaring the "wsx" domain for some tags that are already embedded within some tags from the same domain and the domain is locally declared and not with a global prefix.
- Proposed solution: edit the generated Organization Service stub "OrganizationServiceStub.java", search and replace:
- wsx:MetadataReference by MetadataReference
- wsx:MetadataSection by MetadataSection
- The code samples exposed in this article have been tested only with Dynamics CRM 2011 Online (version 5.0.9690.5010, DB 5.0.9690.3417) and not yet with Dynamics CRM 2013. Some changes will need to be implemented once this new version is officially released.
Conclusion
This article has demonstrated a reliable and quite easy to implement way to integrate Dynamics CRM 2011 Online with Java. The different network frames and algorithms have been detailed so they can be adapted with other languages.
The codes samples of this article have been tested in development but in no mean on a production environment. So prior to implement it in production, adequate testing should be considered and this project will be at your own risks.
The next steps would be to push further this approach to Dynamics CRM On Premise:
- Internet Facing Deployment with ADFS 2.0: this adaptation should be simple given the similarity of the Secure Token Service.
- Windows authentication: this adaptation would be trickier as it would probably need to leverage JAAS (Java Authentication and Authorization Service) and the SPNEGO protocol to authenticate users.
References
Source Code on GitHub
You'll find a full implementation of the approach explained in this article on the following GitHub repository: https://github.com/pdellecase/CRMJavaConnect
Philippe DelleCase's blog
This article is also published on the author's personal blog: <pdellecase.wordpress.com/>
Microsoft Developer Network article: Connect Using Java for Microsoft Dynamics CRM Online
This great article walks you through a basic sample code to integrate Dynamics CRM 2011 Online with Java. The authentication is through LiveID and would not work anymore for recently provisioned Dynamics CRM 2011 Online Organizations.
At the beginning, I was trying hard to generate the Microsoft Dynamics CRM Organization Service stubs with the Java API for XML Web Services (JAX-WS), without any success. This article put me on the right track with the Apache Axis2 library that can parse the Organization Service WSDL without any fatal errors.
Link to the MDN article: msdn.microsoft.com/en-us/library/jj602979.aspx
Microsoft Developer Network article: Download the Endpoints Using the Dynamics CRM Developer Resources Page
This article explains how to download the WSDL from the developer resources pages in the Microsoft Dynamics CRM 2011 and Microsoft Dynamics CRM Online Web application. It provides also some details about the Discovery Service and the Organization Service WSDLs.
Link to the MDN article: msdn.microsoft.com/en-us/library/gg309401.aspx
Microsoft Developer Network article: IOrganizationService Interface > IOrganizationService Members
This article list the public methods members exposed by the IorganizationService interface, with all their parameters, and is a good starting point to find practical samples of code.
Link to the MDN article: msdn.microsoft.com/en-us/library/microsoft.xrm.sdk.iorganizationservice_members.aspx
The Microsoft Dynamics CRM 2011 Software Development Kit (SDK)
The official SDK for Dynamics CRM provided by Microsoft contains many useful .Net samples to get started. I have used some of these samples to monitor, with a tool like Fiddler, the HTTPS traffic between my PC and Microsoft Dynamics CRM in order to gather the authentication request sent to the Office 365 Secure Token Service and the security header from the SOAP requests.
Link to the Microsoft Dynamics CRM 2011 SDK Documentation: msdn.microsoft.com/en-us/library/hh547453.aspx
Link to the Microsoft Dynamics CRM 2011 SDK: www.microsoft.com/en-us/download/details.aspx?id=24004
Comments
Anonymous
September 26, 2013
Hi Robert, Thanks a lot for your article. It helped me a lot to start the CRM integration. I did follow your instruction. "If the application is stateful, the stub can be cached. Once instantiated, the security SOAP header block can be regenerated for each further usages. The cost of this operation is minimal. " But, still the session gets expired after 5 mins and getting verify signature failed error. Thanks, Baskar.SAnonymous
September 29, 2013
Hi Baskar, The stub doesn't have anything to do with the session, it is only an instance of a Java Class. If you follow the instructions, and refresh the header with a clean one (with fresh security header), you should be ok. Please check the other link for this blog to see other comments and to have a faster response to your comments. Thanks.Anonymous
December 22, 2013
The comment has been removedAnonymous
December 24, 2013
Thanks for all the work you have put into this. Since we are using ADFS, is there any sight of a follow up post (or code in github) that demonstrates this working as well ?Anonymous
March 17, 2014
Hi, This was quite useful . I am looking to integrate my product developed in Java with Dynamics CRM 2011 Online. The requirement is to send data captured by the product to CRM . Does Dynamics have any defined APIs for such integrations. ThanksAnonymous
April 11, 2014
Thanks. This is very useful. Perhaps you could add to the 'Troubleshooting' section that the 'wsx:' prefix should also be removed from the DiscoveryServiceStub.java file (as it appears in there as well as in OrganizationServiceStub.java).Anonymous
June 24, 2014
Hi, I am trying to do the same with Internet Facing Deployment with ADFS 2.0.The code can connect to ADFS 2.0 to obtain a token. the 2nd request tries to connect to the CRM organization Service. I get a soap fault : An error occurred when verifying security for the message. Can you Help me to get rid of this error.Anonymous
September 10, 2014
Thank a lot for the tutorial and the code! Works perferctlyAnonymous
November 21, 2014
This blog is really helpful. And I have been able to create an account without any issues. The problem that I face is that I do not know how the AttributeHash for other entities look like. For eg. Lead/Contact/Opportunity etc. If there was any source to get the exact Java Bean collection for all the MS Dynamics entities it would have been really good. Does any one know how to get them? Thanks, ArjunAnonymous
December 15, 2014
sir im a bit new to this so kindly excuse me if i ask simple things. can u explain the process of MESSAGE ID and token ID creation pls. since my client endorses LINUX environment.it woulf be of great help if u can provide some javascript code for generation of required parameters.esp the MessageID and TokenID and an pure SOAP sample xml for create or retrieve along with the 2 security tokens and key identifier.Anonymous
September 29, 2015
What would happen if these security tokens could not be validated? Is anything to perform after every upgrade to CRM, like timeout increase, etc.