Calling Dynamics 365 CRM API from .NET Core 3.1 with MSAL.NET Authentication and Azure AD

Calling Dynamics 365 CRM API from .NET Core 3.1 with MSAL.NET Authentication and Azure AD

By Ivan Krsul, CEO Artexacta S.R.L.

Introduction 

Our objective is to call the Dynamics 365 CRM API from the .NET Core daemon console application using the new MSAL.NET Authentication.  

Microsoft Authentication Library (MSAL, in the Microsoft.Identity.Client package) is the library that's used to sign-in users and request tokens for accessing an API protected by the Microsoft identity platform. Our application requests tokens by using the application's own identity instead of delegated permissions. The authentication flow in this case is known as a client credentials OAuth flow. For more information on how to use MSAL.NET with a client credentials flow, see this article: https://aka.ms/msal-net-client-credentials

 

Azure Application Registration 

We want to be able to connect a .NET Core Console application to the Microsoft Dynamics 365 API.  The Microsoft Dynamics 365 API requires that you register your application in the Azure Active Directory.  Go to https://portal.azure.com and there go to the Azure Active Directory 

In the Active Directory page, you will register your application.  Click on the “App registrations” menu item. 


Click on the “New Registration” item to create your new application  


In the registration page, add the name of your application and select the first radio button, since this application is only going to work for this organization.   We don’t need to put anything on the redirect URI, since this is going to be a daemon console app. 


Our application will be a daemon console application (i.e. something that will not have a user interface) and hance we cannot use the credentials for an existing user.  We will create a Client Secret that will be used by our application. Go to the “Certificate & secrets” section of the application configuration page and click on “New client secret”.  Once you create a secret you will need to copy the secret value because this secret will no longer be displayed in the future: 


Now we need to add permission to sign in and to access Dynamics CRM to this application.  We go to the “API permissions” section and there we click on the “Add a permission” button to add permissions to “Microsoft Graph User. Read” and “Dynamics CRM user impersonation” as shown below. 


To add permissions to Dynamics CRM, click on the “Add a permission” button and from the pop-up menu select Dynamics CRM.  You can also go to “APIs my organization uses” and there search for “Dataverse” 


Once you selected the Dynamics CRM permissions, make sure that you select the “Delegated Permissions” permission type and make sure that you also select the “user_impersonation” permission.  


Once you add the permissions, you need to “Grant admin consent for xxxxxxx. xxx” so that all permissions should have the “Granted for xxxxxxx.xxx” status: 


CRM Application User 

Now we need to ensure that this application is authorized in the Dynamics CRM environment. Go to  https://admin.powerplatform.microsoft.com/environments, select your environment and then go to Settings: 


There, go to the Users + permissions  section and click on “Application users”  


Click on “New app user” and add the application you created in the azure portal to complete the configuration.  


Sample Code 

It turns out that the application registration has useful code that explains some parts of this process.  Go to the Overview section of the application registration and there: 




Once you get to the Quickstart guide, you can download the sample code and get an explanation of how it all fits.   But this is only for the Microsoft Graph API, and we need to connect to the Dynamics 365 API.  


The Dynamics API  

In the code, the line that performs the actual query or operation is highlighted below:  


The query language is described in https://docs.microsoft.com/en-us/powerapps/developer/data-platform/webapi/query-data-web-api but in a nutshell, you can get to the URL for your CRM instance of you go to your environment and select Advanced Settings from the cogwheel in the right 


Then select the settings menu on the top of the page: 


And select Customizations 


In the Customizations page select the Developer Resources item: 


This will take you to a page that spells out the URLs for the CRM Web API. 


In this case, the URL is https://xxxxxxxx.api.crm2.dynamics.com/api/data/v9.2/ and we query by adding to the end of the URL the query command, as detailed in the Web API.   


{"@odata.context":"https:// xxxxxxxx.api.crm2.dynamics.com/api/data/v9.2/$metadata#Microsoft.Dynamics.CRM.WhoAmIResponse","BusinessUnitId":"axxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","UserId":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","OrganizationId":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx"} 

 

Supposedly, the Web https://docs.microsoft.com/en-us/dynamics365/customer-engagement/web-api/transactioncurrency?view=dynamics-ce-odata-9 lists all the entities that can be queried using the API, but not all CRM APIs are not listed here.   







Source Code 

The Test application show below shows how to connect a .NET Core 3.1 Console app to the Dynamics 365 CRM API.   This project uses the following Nuget packages: Microsoft.Identity.Client (Version="4.36.1"), Microsoft.Identity.Web (Version="1.17.0") and Newtonsoft.Json (Version="13.0.1").   All of these are the latest versions and have a long-term support.  

namespace Test 


    class Program 

    { 

        static async Task Main(string[] args) 

        { 

            string clientId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx"; 

            string tenantID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx"; 

            string clientSecret = "*************************************";    

            string apiUrl = "https://xxxxxxxx.crm2.dynamics.com/"; 

 

            // A daemon application is a confidential client application 

            IConfidentialClientApplication app; 

 

            app = ConfidentialClientApplicationBuilder.Create(clientId) 

                .WithClientSecret(clientSecret) 

                .WithAuthority(AzureCloudInstance.AzurePublic, tenantID) 

                .Build(); 

 

            // With client credentials flows the scopes is ALWAYS of the shape "resource/.default", as the  

            // application permissions need to be set statically (in the portal or by PowerShell) and then granted by 

            // a tenant administrator.  

            string[] scopes = new string[] { $"{apiUrl}.default" }; 

 

            AuthenticationResult result = null; 

            try 

            { 

                result = await app.AcquireTokenForClient(scopes) 

                    .ExecuteAsync(); 

                Console.ForegroundColor = ConsoleColor.Green; 

                Console.WriteLine("Token acquired"); 

                Console.ResetColor(); 

            } 

            catch (MsalServiceException ex) when (ex.Message.Contains("AADSTS70011")) 

            { 

                // Invalid scope. The scope has to be of the form "https://resourceurl/.default" 

                // Mitigation: change the scope to be as expected 

                Console.ForegroundColor = ConsoleColor.Red; 

                Console.WriteLine("Scope provided is not supported"); 

                Console.ResetColor(); 

            } 

 

            if (result != null) 

            { 

                var httpClient = new HttpClient(); 

                httpClient.Timeout = new TimeSpan(0, 2, 0); 

                httpClient.BaseAddress = new Uri(apiUrl); 

                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", result.AccessToken); 

 

                string webApiUrl = $"{apiUrl}api/data/v9.2/contacts?$top=10"; 

 

                HttpResponseMessage response = await httpClient.GetAsync(webApiUrl); 

                if (response.IsSuccessStatusCode) 

                { 

                    string json = await response.Content.ReadAsStringAsync(); 

                    JObject result2 = JsonConvert.DeserializeObject(json) as JObject; 

                    Console.ForegroundColor = ConsoleColor.Gray; 

                    foreach (JProperty child in result2.Properties().Where(p => !p.Name.StartsWith("@"))) 

                    { 

                        Console.WriteLine($"{child.Name} = {child.Value}"); 

                    } 

                } 

                else 

                { 

                    Console.ForegroundColor = ConsoleColor.Red; 

                    Console.WriteLine($"Failed to call the web API: {response.StatusCode}"); 

                    string content = await response.Content.ReadAsStringAsync(); 

 

                    // Note that if you got reponse.Code == 403 and reponse.content.code == "Authorization_RequestDenied" 

                    // this is because the tenant admin as not granted consent for the application to call the Web API 

                    Console.WriteLine($"Content: {content}"); 

                } 

                Console.ResetColor(); 

            } 

        } 

    } 





Comentarios