Recently I’ve been working on a p2p integration between Dynamics 365 Finance and another cloud
based solution that uses OAuth with a callback URI requirement.
The basic authentication process is:
1. Send Auth code request from Dynamics to External System (include callback URI)
2. Redirect to Login page for External system to approve authentication
3. Receive Auth code via callback URI
4. Use Auth code to request access token from external system (including original callback URI)
5. Use Token response and token refresh code to get new token when the existing token expires
6. Make API calls using Token
Steps 1 to 3 are what we need a solution for, as Dynamics 365 Finance and Operations does not have a
callback URI we can make use of and all endpoints exposed from Dynamics (whether ODATA or
custom services require AAD authentication).
There are a few options to provide a valid callback URI, the first few that spring to mind would be to utilise Power Automate or LogicApps which can be exposed as an HTTP endpoint but those endpoint URLs did not match our requirement of which was that it can’t contain variable subdomains, paths or queries.
Keeping with the Microsoft stack and utilising the Azure subscription we have in place there were 2 solutions I found that could provide the required endpoint and also process additional logic to capture the Auth code and store it inside Dynamics.
1. Azure Function App
This was my initial thought when it came to a serverless solution that could capture the HTTP callback response and then publish the appropriate data (authorisation code) straight to Dynamics using a webservice call.
Pros:
Azure functions can be written to do almost anything.
1 Million free calls
Server-less
Cons:
Azure storage required to store code and referenced DLLs (this costs extra)
Pricing details here: https://azure.microsoft.com/en-us/pricing/details/functions/
2. Azure API Management Service
This one was completely new to me, but provides a lot of functionality packed into a serverless service.
Pros:
HTTPS endpoint and pre-defined inbound/outbound processing structure.
1 Million free calls (Consumption pricing)
Server-less
Cons:
Does not have the same flexibility and overall functionality when it comes to writing code as an Azure Function App.
Pricing details here: https://azure.microsoft.com/en-us/pricing/details/api-management/
I have tested both solutions and although I decided to use the API management service for my scenario, had I required any more complicated functionality Azure Function Apps would have been the better option.
Azure Function App Solution
The major benefit I found of the Azure Function App option was that I could effectively copy and paste code from a local .NET project that integrates with a Dynamics 365 environment and use that existing logic to publish my Auth code through into Dynamics.
This can be done directly inside a browser in the Azure Portal or inside Visual Studio Code.
Note: to reference any dlls and assemblies you will need to upload them to your linked blob storage account (in this case the Microsoft.IdentityModel.Clients.ActiveDirectory.dll). This is where you start incurring costs as you increase your storage use.
You can test your function as you go with parameters that will be supplied to the callback URL:
The Azure Function exposes a callback URL which is available at the top of the editor
You can then call the URL directly and confirm that it performs the expected action inside Dynamics (in my example I have a new data entity that I am writing the code to)
Azure Managed API Service Solution
This solution requires less code, but I did find debugging issues to be quite difficult. It also appears to not be designed to use it the way I did (receiving an HTTP GET request and using that to trigger an HTTP POST).
However this ended up being a neat little solution that provides the exact same functionality as the Azure Function App without any storage account costs.
The first step for this is to create a new Azure API Management Service in the Azure Portal
Once the new service is published you will receive a notification in the Azure Portal and can move onto the next step of configuring the APIs.
You might want to go and get a coffee at this point.. it does take quite a while to activate the service
Setup your environment connection variables under “Named values”
Auth server should be https://login.microsoftonline.com/TENANTDOMAIN/oauth2/v2.0/token
Scope should be https://d365environmentURL/.default
Store both your Application/Client ID and Secret here as well
Then go to the APIs and create a new blank one
Your Web service URL should be the service you want to pass your message on to.
You can then edit your API
First add an operation and during creation or afterwards by choosing the form based editor for the front-end you can set your Query parameters, then by clicking to edit the inbound processing code you can connect to the Authentication service and get your token for the outbound webservice call to Dynamics.
This is the code I use in the base to get the header authorization token and set it as the header and set the JSON body for the outbound webservice call.
<policies> <inbound> <base /> <send-request ignore-error="true" timeout="20" response-variable-name="bearerToken" mode="new"> <set-url>{{authorizationServer}}</set-url> <set-method>POST</set-method> <set-header name="Content-Type" exists-action="override"> <value>application/x-www-form-urlencoded</value> </set-header> <set-body>@{ return "client_id={{clientId}}&scope={{scope}}&client_secret={{clientSecret}}&grant_type=client_credentials"; }</set-body> </send-request> <set-header name="Content-Type" exists-action="override"> <value>application/json</value> </set-header> <set-header name="Authorization" exists-action="override"> <value>@("Bearer " + (String)((IResponse)context.Variables["bearerToken"]).Body.As<JObject>()["access_token"])</value> </set-header> <!-- Don't expose APIM subscription key to the backend. --> <set-header name="Ocp-Apim-Subscription-Key" exists-action="delete" /> <set-method>POST</set-method> <set-body template="liquid"> { "code": "{{context.Request.OriginalUrl.Query.code}}", "state": "{{context.Request.OriginalUrl.Query.state}}" } </set-body> </inbound> <backend> <base /> </backend> <outbound> <base /> </outbound> <on-error> <base /> </on-error> </policies>
Now to test the callback URL.
Against the operation there is a TEST tab which allows you to set the query parameter values and perform a test call
Alternatively you can get the URL from the settings tab of the operation and you can run a browser test to confirm it works.
And that’s it – two ways of creating a callback URL for authentication with an external system
For those who want the code for the Azure function App – here it is:
(remember to upload the IdentityModel dll to your site’s bin folder using the Kudu debug console – https://yourfunction.scm.azurewebsites.net/DebugConsole/?shell=powershell).
#r "Newtonsoft.Json" #r "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" using System.Net; using System.IO; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; using Newtonsoft.Json; using Microsoft.IdentityModel.Clients.ActiveDirectory; public static async Task<IActionResult> Run(HttpRequest req, ILogger log) { log.LogInformation("C# HTTP trigger function processed a request."); string code = req.Query["code"]; string state = req.Query["state"]; string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); dynamic data = JsonConvert.DeserializeObject(requestBody); code = code ?? data?.code; state = state ?? data?.state; string aadTenant = System.Environment.GetEnvironmentVariable("ActiveDirectoryTenant"); string aadClientAppId = System.Environment.GetEnvironmentVariable("ActiveDirectoryClientAppId"); string aadClientAppSecret = System.Environment.GetEnvironmentVariable("ActiveDirectoryClientAppSecret"); string aadResource = System.Environment.GetEnvironmentVariable("ActiveDirectoryResource"); string uri = System.Environment.GetEnvironmentVariable("URIString"); AuthenticationContext authenticationContext = new AuthenticationContext(aadTenant, false); AuthenticationResult authenticationResult; try { // OAuth through application by application id and application secret. var credential = new ClientCredential(aadClientAppId, aadClientAppSecret); authenticationResult = authenticationContext.AcquireTokenAsync(aadResource, credential).Result; } catch (Exception ex) { Console.WriteLine(string.Format("Failed to authenticate with AAD by application with exception {0} and the stack trace {1}", ex.ToString(), ex.StackTrace)); throw new Exception("Failed to authenticate with AAD by application."); } string _body = '{' + string.Format(@"""code"" : ""{0}"", ""state"": ""{1}""", code, state) + '}'; string jwt = authenticationResult.CreateAuthorizationHeader(); string AUTHORIZATION_HEADER_NAME = "Authorization"; var webRequest = System.Net.WebRequest.Create(uri); if(jwt.Length > 0) { System.Net.WebHeaderCollection headers = webRequest.Headers; headers.Add(AUTHORIZATION_HEADER_NAME,jwt); webRequest.Headers = headers; } webRequest.Method = "POST"; webRequest.Timeout = -1; if(_body.Length > 0) { var messageBytes = System.Text.Encoding.UTF8.GetBytes(_body); webRequest.ContentType = "application/json";//text/plain"; webRequest.ContentLength = messageBytes.Length; var messageStream = webRequest.GetRequestStream(); messageStream.Write(messageBytes, 0, messageBytes.Length); messageStream.Close(); } try { using (var webResponse = webRequest.GetResponse()) { var responseReader = new System.IO.StreamReader(webResponse.GetResponseStream()); _body = responseReader.ReadToEnd(); } } catch { return new BadRequestObjectResult($"Fail, {_body}"); } return code != null ? (ActionResult)new OkObjectResult($"OK, {_body}") : new BadRequestObjectResult("Please pass a code on the query string or in the request body"); }
Dynamics 365 F&O Callback