Tag: Microsoft MVP

Authenticate Dynamics 365 in Azure Functions using MSAL

As you all know, ADAL is deprecated and will be unsupported by June 30th 2022, Microsoft recommends to use the MSAL (Microsoft Authentication Library). MS has also advised to migrate your existing applications to MSAL.

MSAL makes it easy for developers to add identity capabilities to their applications. With just a few lines of code, developers can authenticate users and applications, as well as acquire tokens to access resources. MSAL also enables developers to integrate with the latest capabilities in our platform—like passwordless and Conditional Access.

Microsoft Build 2020

Read the official FAQ for migrating your applications to MSAL here

MSAL is now the recommended official authentication library for use with the Microsoft identity platform.

Photo by Ilargian Faus on Pexels.com

In my pervious post Authenticate Dynamics 365 in Azure Functions Version 3 , I used ADAL for authentication. The below code authenticates D365 using MSAL, and use WEBAPI to fetch the contact record with the email address passed in the request body.

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Microsoft.Identity.Client;
using System.Net.Http.Headers;
using System.Net.Http;

namespace AzureFunctionMSAL
{
    public static class ConnectMSAL
    {
       

        [FunctionName("MSALFunction")]

        // change get or post based on your scenario

        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            string contactEmail = req.Query["eamil"];
            string contacts = string.Empty;
            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            dynamic data = JsonConvert.DeserializeObject(requestBody);
            contactEmail = contactEmail ?? data?.email;


            // MSAL Authentication
            string clientId = "*********583";
            string secret = "zDC******";
            string[] scope = new string[] { "https://******.crm.dynamics.com/.default" };
            string webAPI = " https://***.crm.dynamics.com/api/data/v9.1/";
            string authority = "https://login.microsoftonline.com/69e9641e-4be0-4f4c-9ae4-06fdc1160c34";

            var clientApp = ConfidentialClientApplicationBuilder.Create(clientId: clientId)
            .WithClientSecret(clientSecret: secret)
            .WithAuthority(new Uri(authority))
            .Build();

            try
            {
                AuthenticationResult authResult = await clientApp.AcquireTokenForClient(scope).ExecuteAsync();

    // WEB API
                var httpClient = new HttpClient();
                httpClient.DefaultRequestHeaders.Add("OData-MaxVersion", "4.0");
                httpClient.DefaultRequestHeaders.Add("OData-Version", "4.0");
                httpClient.Timeout = new TimeSpan(0, 2, 0);
                httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
                httpClient.BaseAddress = new Uri(webAPI);
                var queryOptions = "contacts?$select=fullname,mobilephone&$filter=emailaddress1 eq '" + contactEmail + "'";
                var response =  httpClient.GetAsync(queryOptions).Result;
                
                if (response.IsSuccessStatusCode)
                {
                    var contactsresult = response.Content.ReadAsStringAsync().Result;
                    contacts = contactsresult;
                    log.LogInformation("webapi executed");
               
                }

            }
            catch (Exception ex)
            {
                // log error 
                string errorMessage = ex.Message;
                log.LogError(errorMessage);
                //response
                return new BadRequestObjectResult(JsonConvert.SerializeObject("an error occured while retriving contacts:"));
            }



            //
            string responseMessage = "WEB API Response: " + contacts;

            return new OkObjectResult(responseMessage);
        }
    }
}

Test your function from post man.

Request
Response

Please note, the response in the code is not well formatted, you may format the response as a proper json object based on your business need. You might also need to add required exception handling in the code.

A short explanation on the code.

Hope this helps.

Use Dynamics 365 WEB API in Azure Functions Version 3

In the previous post, we saw how to authenticate Dynamics 365 in Azure Functions runtime version 3 (.NET Core). Now let’s see how to use Dynamics 365 WEB API after acquiring the bearer token.

The following sample code accepts email as input in the request body, and uses Web api to return the contacts records with the passed email.

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Net.Http;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System.Net;
using System.Net.Http.Headers;

namespace SampleFunctionApp
{
   

    public static class D365
    {
        [FunctionName("ConnecttoD365")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            string contactEmail = req.Query["email"];
            string responseMessage = string.Empty;
            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            dynamic data = JsonConvert.DeserializeObject(requestBody);
            contactEmail = contactEmail ?? data?.email;
            #region Auth Code
            //
            string cloud = "https://login.microsoftonline.com";

            //This is the Domain!

            string tenantId = "*******";

            string authority = $"{cloud}/{tenantId}";

            // ApplicationID in the new UI

            string clientId = "************";

            //Created from scratch in Keys

            string clientsecret = "*************";

            ClientCredential clientcred = new ClientCredential(clientId, clientsecret);

            // Application ID of the Resource (could also be the Resource URI)

            string resource = "https://*****.crm.dynamics.com/";

            AuthenticationContext ac = new AuthenticationContext(authority);
            AuthenticationResult result = null;
            var bearerToken = string.Empty;
            string ErrorMessege = string.Empty;
            try

            {
                //already having token
                result = await ac.AcquireTokenSilentAsync(resource, clientId);
                if (result != null)
                {
                    bearerToken = result.AccessToken;
                    log.LogInformation("Token Acquired:" );
                }
            }

            catch (AdalException adalException)

            {//Acquire token
                if (adalException.ErrorCode == AdalError.FailedToAcquireTokenSilently

                || adalException.ErrorCode == AdalError.InteractionRequired)

                {

                    result = await ac.AcquireTokenAsync(resource, clientcred);
                    if (result != null)
                    {
                        bearerToken = result.AccessToken;
                        log.LogInformation("Token Acquired");

                    }
                }
                else
                {
                    log.LogWarning("Failed to acquire Bearer Token :-" + adalException.Message);
                    var AdalException = new { adalexception = "Failed to acquire Bearer Token :-" + adalException.Message };
                    return new BadRequestObjectResult(JsonConvert.SerializeObject(AdalException));
                    throw adalException;

                }

            }

            #endregion

            #region getContact
           


            string outputString = string.Empty;
            //Next use a HttpClient object to connect to specified CRM Web service.
            var httpClient = new HttpClient();
            //Define the Web API base address, the max period of execute time, the 
            // default OData version, and the default response payload format.
            httpClient.BaseAddress = new Uri("https://*********.crm.dynamics.com/api/data/v9.1/");
            httpClient.Timeout = new TimeSpan(0, 2, 0);
            httpClient.DefaultRequestHeaders.Add("OData-MaxVersion", "4.0");
            httpClient.DefaultRequestHeaders.Add("OData-Version", "4.0");
            httpClient.DefaultRequestHeaders.Add("Prefer", "odata.include-annotations=\"*\"");
            httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", (bearerToken));

            //Query to retrieve contacts
            var queryOptions = "contacts?$select=fullname,mobilephone&$filter=emailaddress1 eq '" + contactEmail + "'";
            HttpResponseMessage retrieveResponse1 = httpClient.GetAsync(queryOptions).Result;


            if (retrieveResponse1.StatusCode == HttpStatusCode.OK)
            {


              

                    responseMessage = retrieveResponse1.Content.ReadAsStringAsync().Result.ToString();
               

            }
            else
            {

                return new BadRequestObjectResult(JsonConvert.SerializeObject("an error occured while retriving contacts"));
            }



            #endregion

            if (string.IsNullOrEmpty(contactEmail) && responseMessage== String.Empty)
            {
                responseMessage = "Pass an email in the query string or in the request body";
            }        

            return new OkObjectResult(responseMessage);
        }
    }
}

Test your function from post man.

Request
Response

Please note, the response in the code is not well formatted, you may format the response as a proper json object based on your business need.

Hope this helps.

Call JavaScript Methods Globally Over Dashboards, Home Grids, and Record Forms.

Recently, I received a strange requirement to show a dialogue to users as soon as they login to Dynamics 365. The dialogue can be an HTML page with company events or a video stream (exact requirement not mentioned here). The pop-up should be shown over dashboards as well as entity home grids. It is easy to show the dialogue on a record form as I can register JavaScript on the record form, or show an application-level notification using addGlobalNotification client API, if it was just a notification only. I was stuck on showing the dialogue on dashboards, as most model driven apps in D365 will land on some dashboard when the user logs-in, and we do not have any event triggers there. I shared the requirement with the community experts and finally Linn Zaw Win, Microsoft Business Application MVP, gave me a lead to try using the application ribbon buttons. Booom!!! It worked.

Photo by Andrea Piacquadio on Pexels.com

Here is what I tried.

  1. Added a hidden button in Global Tab (where the Advanced Find button is located) using Ribbon Workbench.
  2. Added JavaScript function using CustomRule of the button Enable Rule.
  3. Triggered dialogue by adding required logic inside my enable rule JavaScript.
  4. Since the global tab is there on all screens of the model driven app (including dashboard and grids), the button enable rule JavaScript method will be called and so is our dialogue logic.

Add a new button in the global tab.

Create a new solution and add application ribbon and one entity (any entity is fine) to the solution. Now open your solution using ribbon workbench. Select ApplicationRibbon in the entity dropdown as shown below.

Add a new button in the Global Tab by scrolling the Home area to the right until you find the Mscrm.GlobalTab. Drag a button and drop into  Mscrm.GlobalTab.

Add enable rule for the button.

In the EnableRule, add a new step as CustomRule and select the JavaScript web resource in the library and enter the function name. Set default value as False so that button will be hidden (assuming your JavaScript function is not returning “True” value), you can also use display rules to keep the button always hidden.

Create a new Command and add the new enable rule we just created.

Now associate this command with the newly created button.

Publish your button

Once you have published, whenever a Dynamics 365 page is loaded, system will call your enable JavaScript. Below is the sample code I have used for showing a simple alert.

 function  showGlobalMessage()
	{
		 var alertStrings = { confirmButtonLabel: "Yes", text: "Go to Todays Video?", title: "Your Daily Message" };
var alertOptions = { height: 120, width: 260 };
Xrm.Navigation.openAlertDialog(alertStrings, alertOptions)
return false; 
}

Note: As per the above logic, the dialogue shows (enable rule will be triggered) every time the user opens a new tab/window. To show the dialogue only once or with specific intervals, you can use a new custom entity to keep track of the notification setting for each user using Xrm.WebApi, or you can use  localStorage (but using local storage may not be an officially supported D365 customization)

I know this is not a common scenario or ideal use case, but I hope it will save you some time when you come across strange requirements 🙂