Philippsen's Blog

Everyday findings in my world of .net and related stuff

Posts Tagged ‘WCF’

How to implement basic authentication in Dynamics AX2012 webservice

Posted by Torben M. Philippsen on February 26, 2016

Recently I had to configure an inbound port for basic authentication. Furthermore I had to produce a guide in order to being able to set it up in the same way in other environments.

Lucky for me, I found this great guide, that I would like to share. You can also use it as a template for configuring other types of authentication.

https://blogs.msdn.microsoft.com/axsupport/2013/04/23/how-to-implement-basic-authentication-with-dynamics-ax-2012-web-services/

Advertisements

Posted in AIF, Microsoft Dynamics AX | Tagged: , , , | Comments Off on How to implement basic authentication in Dynamics AX2012 webservice

A call to SSPI failed–DynAx 2012 AIF/WCF

Posted by Torben M. Philippsen on January 13, 2015

Recently I deployed an AIF service to a customer environment. Everything was working fine in my single server development environment, but after deploy to the distributed customer environment, calls to the webservice resulted in the error “A call to sspi failed”.

The scenario:

  • My service – a simple document service. No hex about that.
  • I needed to deploy the service to IIS in order for it to be consumable from a corporate website
  • The customer environment contained a standalone server for the AOS and a standalone server for IIS
  • I created a simple test webform – my test client, in order to being able to test that everything was working ok.

Having deployed the service, the service was browsable. The identity of the application running the AIF site was the same as the one used for the Business connector proxy account (System administration –> setup –-> service accounts) The app pool was configured like this:

AppPoolConfig

Authentication was configured like this:

Authentication

Here’s a nice reference on how to install AIF on IIS when using Ax2012

From AX my service was configured to use a customBinding using NTLM and my clienct was also configured to use NTLM. Any call from the client to the service would result in the error “A call to SSPI failed – see inner exception…” – and no inner exception were to be found.

Trying to narrow down the problem a basicHTTPBinding was tried – still the same error.

As different kinds of blogposts suggested, I was able to call the AIF/WCF service when the service itself was using the ipaddress (to avoid the use of kerberos) of the aos server instead of the url. However this wasn’t an acceptable solution, as any new deployment of the service from AX, would result in a non working webservice, since the web.config would be overwritten when deploying from AX. And as it turned out, it was not possible to alter settings in AX forcing ax to deploy the service and having the endpoint in web.config reference the ip address instead af the FQDN. However the problem was now narrowed down to be caused by kerberos. I found this great blogpost explaining some basic things about Kerberos.

Another thing we tried out was to set the spn for the user running the service:

Setspn –A HTTP/2012webtest.myDomain.local myDomain\sa-proxy-lon

Having done that we tried to setup trust for delegation in AD according to this. We are not sure whether this had any effect, but we didn’t reverse the process.

This blogpost (see comment from Eric Ledoux and Brian Kinser) suggested that this might be caused by a kernel error. My customer recently upgraded to R2CU7 and I was expecting this to be fine, but talking with the technician from the customer revealed that IIS might not have been updated in that process with the new AX components. Running the setup file from the CU7 install media, suggested to update some core AX components. Choosing yes to update, restarting IIS and the AOS service, fault messages from ax started to show up when calling the webservice – meaning that everything was starting to work as expected.

Conclusion

In my case the “a call to sspi failed” error turned out to be resolved when upgrading to CU7. The problem I was facing was just caused by the fact that only the AOS had been upgraded – not IIS. Resolving this mismatch solved the problem.

Thanks to my colleague Morten Uldall for both moral and technical support:-)

Posted in AIF, Microsoft Dynamics AX | Tagged: , , , , | Comments Off on A call to SSPI failed–DynAx 2012 AIF/WCF

Digging into AIF

Posted by Torben M. Philippsen on November 29, 2011

Given my experiance in WCF I recently had the pleasure to dig into AIF (the Application Integration Framework in Dynamics AX).

As a newbie I quickly had to learn my lessons – and to avoid having to learn the same lessons twice I decided to add this post:-)

Initially I had to realize that these questions needed an answer:

  • Inside Dynmics AX – what is an endpoint?
  • Inside AX – what’s the idea of having local endpoints along with endpoints – what’s the difference?
  • How do I target a specific company?
  • How do I configure my .net client to use a certain AX endpoint?
  • How do I make sure that a certain datapolicy is actually applied to incoming messages?

In my examples I will use the build in LedgerPurchaseInvoiceService.

Asuming that this service has been enabled in “basic -> setup -> Application Integration Framework -> services” go to “basic -> setup -> Application Integration Framework -> endpoints”. There You will at least find the default Endpoint. AX uses the “Default Endpoint” as the destination endpoint and the local endpoint of the user calling the service for processing WCF requests. I wanted to be able to specify which endpoints my requests were using in order to take advantage of the security and processing features in AIF, This means that if no endpoint is specified from the client side when sending messages through AIF, the default endpoint is used. This also means that if You configure Your own endpoints these need to be specified from the client side…

We need to answer some of the questions above:

  1. What is an endpoint?
    Well I will try to explain this in my own terms… An endpoint is the single point of entry that is used from the client side. An endpoint is associated to a company which means if you configure an endpoint in company “A” you won’t be able to use that endpoint from comapny “B”.
    The endpoint is where you define the datapolicies – which fields are enabled, which fields are required and so on…
    An endpoint is based on a local endpoint.
  2. What’s the idea of having local endpoints?
    Local endpoints are global:-) This means that any of the local endpoints are visible through out all companies. A local endpoint is used to point to a certain company. Which brings us to question number 3
  3. If I want to target a certain company from the client side I will have to point to an endpoint configured in that company that uses a local endpoint associated to the same company.
    endpoints

    endpoints

    local_endpoints

    local_endpoints

    In my case I want to use the “SignFlow” endpoint from the client – which brings us to question number 4

  4. How do I configure my .net client to use a certain AX endpoint?
    The  magic word is “SOAP Headers”. Before calling any of the functions of your client object. You need to specify the destination endpoint, the sourceEndpoint and the sourceEndpointUser. The last two headers requires that You use a binding type that supports WS-Addressing.
    To add the SOAP Header You do the following:
 			PurchaseInvoiceServiceClient client = new PurchaseInvoiceServiceClient ();
 			try
 			{
 				//SOAP header info
 				using (new OperationContextScope (client.InnerChannel))
 				{
 					//CREATE HEADER TO SET SOURCE ENDPOINT
 					//this assumes that a endpoint (inside AX) with the name [SignFlow] has been created for all companies
 					SetDestinationEndpoint(ddlDataAraeId.SelectedValue);
 					//CREATE HEADER TO SET TARGET COMPANY
 					//this assumes that a local endpoint (inside ax) with the name [ddlDataAraeId.SelectedValue] has been configured
 					//and is associated with a company that exists in dynamics ax
 					SetSourceEndpointAndUser("SignFlow" );
 					//submit the request and retrieve the respons 
 					keys = client.create(recordWrapper);
 				}
 			}

 		/// <summary>
 		/// Helper method - adds a SOAP Header defining the destination endpoint (local endpoint) in Dynamics AX
 		/// </summary>
 		/// <param name="nameOfEndpoint">The name of the local endpoint</param>
 		private void SetDestinationEndpoint(string nameOfEndpoint)
 		{
 				OperationContext .Current.OutgoingMessageHeaders.Add(MessageHeader .CreateHeader("DestinationEndpoint" , "http://schemas.microsoft.com/dynamics/2008/01/services" , nameOfEndpoint)); 
 		}

 		/// <summary>
 		/// Helper method - adds a SOAP Header defining the source endpoint name and the source endpoint user to use
 		/// </summary>
 		/// <param name="sourceEndpointName">the name of the source endpoint</param>
 		private void SetSourceEndpointAndUser(string sourceEndpointName)
 		{
 			string userName = HttpContext .Current.User.Identity.Name.ToString(); //returns the current user and domian - eg. domainname\\username
 			var addressHeader = AddressHeader .CreateAddressHeader("SourceEndpointUser" , "http://schemas.microsoft.com/dynamics/2008/01/services" , userName);
 			var addressBuilder = new EndpointAddressBuilder (
 			new EndpointAddress (new Uri ("urn:" + sourceEndpointName), addressHeader));
 			var endpointAddress = addressBuilder.ToEndpointAddress();
 			OperationContext .Current.OutgoingMessageHeaders.From = endpointAddress;
 		}

The entire client code looks like this:

 using System;
 using System.Web;
 using System.Web.UI;
 using System.ServiceModel;
 using System.ServiceModel.Channels;
 using SignFlowIntegration.EG_InitialPurchaseInvoiceReg;

 namespace SignFlowIntegration
 {
 	public partial class InitialReg : System.Web.UI.Page
 	{
 		private enum DataAreaIdCollection { SVE, DMO, DAT, LON, MAS, FIN };
 		private enum CurrencyCodeCollection { DKK, AUD, CHF, CAD, EUR, GBP, NOK, SEK, USD, PLN };
 		protected void Page_Load(object sender, EventArgs e)
 		{
 			if (!Page.IsPostBack)
 			{
 				ddlDataAraeId.DataSource = System.Enum .GetValues(typeof (DataAreaIdCollection ));
 				ddlCurrencyCode.DataSource = System.Enum .GetValues(typeof (CurrencyCodeCollection ));
 				ddlDataAraeId.DataBind();
 				ddlCurrencyCode.DataBind();
 			}
 		}

 		/// <summary>
 		/// Handles the submit button click event
 		/// </summary>
 		/// <param name="sender"></param>
 		/// <param name="e"></param>
 		protected void btnSubmit_Click(object sender, EventArgs e)
 		{
 			decimal amountCur;
 			DateTime documentDate;
 			//create the client
 			PurchaseInvoiceServiceClient client = new PurchaseInvoiceServiceClient ();
 			//create wrapper for record object
 			AxdPurchaseInvoice recordWrapper = new AxdPurchaseInvoice ();
 			//create record objects
 			AxdEntity_LedgerJournalTable ledgerJournalTableObj = new AxdEntity_LedgerJournalTable ();
 			AxdEntity_LedgerJournalTrans ledgerJournalTransObj = new AxdEntity_LedgerJournalTrans (); 
 			//-----------------------------------------
 			//step 1
 			//-----------------------------------------
 			//assign values to record objects
 			ledgerJournalTableObj.JournalName = txtJournalName.Text;
 			ledgerJournalTableObj.JournalTypeSpecified = true ;
 			ledgerJournalTableObj.JournalType = AxdEnum_LedgerJournalType .PurchaseLedger;
 			ledgerJournalTransObj.AccountNum = txtAccountNum.Text;
 			ledgerJournalTransObj.EG_SignFlowCaseId = txtSignFlowsagsnr.Text;
 			if (DateTime .TryParse(txtDocumentDate.Text, out documentDate))
 			{
 				//set documentdate
 				ledgerJournalTransObj.DocumentDate = documentDate;
 			}
 			else
 			{
 				//default
 				ledgerJournalTransObj.DocumentDate = DateTime .Now;
 			}
 			ledgerJournalTransObj.CurrencyCode = ddlCurrencyCode.SelectedValue;
 			ledgerJournalTransObj.ApprovedBy = txtApprovedBy.Text;
 			ledgerJournalTransObj.Invoice = txtInvoice.Text;
 			if (decimal .TryParse(txtAmountCurCredit.Text, out amountCur))
 			{
 				ledgerJournalTransObj.AmountCurCreditSpecified = true ;
 				ledgerJournalTransObj.AmountCurCredit = amountCur;
 			}
 			else if (decimal .TryParse(txtAmountCurDebit.Text, out amountCur))
 			{
 				ledgerJournalTransObj.AmountCurDebitSpecified = true ;
 				ledgerJournalTransObj.AmountCurDebit = amountCur;
 			}
 			else
 			{
 				//default
 				ledgerJournalTransObj.AmountCurCreditSpecified = true ;
 				ledgerJournalTransObj.AmountCurCredit = 0;
 			}
 			//-----------------------------
 			//step 2 - add record to wrapper
 			//-----------------------------
 			recordWrapper.LedgerJournalTable = new AxdEntity_LedgerJournalTable [1]; 
 			ledgerJournalTableObj.LedgerJournalTrans = new AxdEntity_LedgerJournalTrans [1];
 			ledgerJournalTableObj.LedgerJournalTrans[0] = ledgerJournalTransObj;
 			recordWrapper.LedgerJournalTable[0] = ledgerJournalTableObj;
 			//Set impersonation level
 			client.ClientCredentials.Windows.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel .Impersonation;
 			//prepare the repons object
 			EntityKey [] keys = new EntityKey [1];
 			//--------------------------------------
 			//step 3 - call create method on service
 			//add proper error handling
 			//--------------------------------------

 			try
 			{
 				//SOAP header info
 				using (new OperationContextScope (client.InnerChannel))
 				{
 					//CREATE HEADER TO SET SOURCE ENDPOINT
 					//this assumes that a endpoint (inside AX) with the name [SignFlow] has been created for all companies
 					SetDestinationEndpoint(ddlDataAraeId.SelectedValue);
 					//CREATE HEADER TO SET TARGET COMPANY
 					//this assumes that a local endpoint (inside ax) with the name [ddlDataAraeId.SelectedValue] has been configured
 					//and is associated with a company that exists in dynamics ax
 					SetSourceEndpointAndUser("SignFlow" );
 					//submit the request and retrieve the respons 
 					keys = client.create(recordWrapper);
 				}
 			}
 			catch (System.ServiceModel.FaultException <AifFault > aex)
 			{

 				//TODO add proper error handling
 				txtResult.Text += "AIF ERROR\\n" ;
 				txtResult.Text += "Message: " + aex.Message + "\\n" ;
 				txtResult.Text += "Source: " + aex.Source + "\\n" ;
 				txtResult.Text += "StackTrace: " + aex.StackTrace + "\\n" ;
 				txtResult.Text += "----------------------------------------\\n" ;
 				//txtResult.Text += aex.Detail.FaultMessageListArray[0].FaultMessageArray[0].Message;
 			}
 			catch (System.ServiceModel.FaultException fex)
 			{
 				//SOAP errors are handled here
 				//TODO add proper error handling
 				txtResult.Text += "Fault ERROR\\n" ;
 				txtResult.Text += "Message: " + fex.Message + "\\n" ;
 				txtResult.Text += "Source: " + fex.Source + "\\n" ;
 				txtResult.Text += "StackTrace: " + fex.StackTrace + "\\n" ;
 			}
 			catch (Exception ex)
 			{
 				//TODO add proper error handling
 				txtResult.Text += "ERROR\\n" ;
 				txtResult.Text += "Message: " + ex.Message + "\\n" ;
 				txtResult.Text += "Source: " + ex.Source + "\\n" ;
 				txtResult.Text += "StackTrace: " + ex.StackTrace + "\\n" ;
 			}
 			finally
 			{
 				//close the client
 				client.Close();
 			}

 			//----------------------------------
 			//Step 4 - Handle the response
 			//----------------------------------
 			//examine the respons - is it valid?
 			if (keys[0] != null ) // the repons contains info ->'s valid
 			{
 				txtResult.Text += "SUCCESS\\n" ;
 				KeyField [] fields = keys[0].KeyData;
 				foreach (KeyField field in fields)
 				{
 					//print the result
 					txtResult.Text += field.Field + ": " + field.Value + "\\n" ;
 				}
 			}
 		}

 		/// <summary>
 		/// Helper method - adds a SOAP Header defining the destination endpoint (local endpoint) in Dynamics AX
 		/// </summary>
 		/// <param name="nameOfEndpoint">The name of the local endpoint</param>
 		private void SetDestinationEndpoint(string nameOfEndpoint)
 		{
 				OperationContext .Current.OutgoingMessageHeaders.Add(MessageHeader .CreateHeader("DestinationEndpoint" , "http://schemas.microsoft.com/dynamics/2008/01/services" , nameOfEndpoint)); 
 		}

 		/// <summary>
 		/// Helper method - adds a SOAP Header defining the source endpoint name and the source endpoint user to use
 		/// </summary>
 		/// <param name="sourceEndpointName">the name of the source endpoint</param>
 		private void SetSourceEndpointAndUser(string sourceEndpointName)
 		{
 			string userName = HttpContext .Current.User.Identity.Name.ToString(); //returns the current user and domian - eg. egdk\\tomph
 			var addressHeader = AddressHeader .CreateAddressHeader("SourceEndpointUser" , "http://schemas.microsoft.com/dynamics/2008/01/services" , userName);
 			var addressBuilder = new EndpointAddressBuilder (
 			new EndpointAddress (new Uri ("urn:" + sourceEndpointName), addressHeader));
 			var endpointAddress = addressBuilder.ToEndpointAddress();
 			OperationContext .Current.OutgoingMessageHeaders.From = endpointAddress;
 		}

 	}

 }

Please remember that You have to use a bindign that support WS-Addressing. You can either use any of the pre defined ws bindings or use the “custom binding”. In my case I have used the custom binding, which is the most easy one to configure if You just want to test this out. Just set the Authentication property to NTLM instead of anonymous – we are not interested in anonymous user in AX.

Please keep in mind, that if Your setup allows it, You are always able to use the default endpoint, which means that all the SOAP header stuff can be left out of the equation.
Thanks to my collegue Michael Cronqvist for assisting me and also great thanks to my former collegue Tue Theilmann Jørgensen for pointing me in the SOAP Headers direction.

These aditional ressources might come in handy:

Posted in AIF, Microsoft Dynamics AX | Tagged: , , , , , , | Comments Off on Digging into AIF

 
%d bloggers like this: