Service to service authentication in Business Central 18.3 – How to use in C#

4 Aug

We continue the series about Service to Service authentication, aka Client Credentials Flow, with some tips about getting and using the access token with C#.

Please see the previous posts in this series for more information about how to set up and test the Client Credentials Flow:

In this post, I want to look at C# and give some tips on retrieving an access token and using it to call a Business Central API. All example code below is just a .Net 5.0 Console application. It should be pretty easy to apply the code to an Azure Function or any other program you may have. The required values for Client Id, Client Secret, and AAD Tenant ID are defined as variables to keep the code as simple as possible, but you may consider putting them into a config file or maybe an Azure Key Vault.

Retrieve access token with raw request

Let’s first look at what I named the ‘raw request’. It’s basically a copy of the request from the Powershell example. The HttpClient is used to call to the Azure token endpoint. There is no third-party component being used, everything is standard .Net code.

using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;

namespace OAuthClientCredentialsDemoRaw
{
    class Program
    {
        private const string ClientId = "3870c15c-5700-4704-8b1b-e020052cc860";
        private const string ClientSecret = "~FJRgS5q0YsAEefkW-_pA4ENJ_vIh-5RV9";
        private const string AadTenantId = "kauffmann.nl";
        private const string Authority = "https://login.microsoftonline.com/{AadTenantId}/oauth2/v2.0/token";

        static void Main(string[] args)
        {
            string accessToken = GetAccessToken(AadTenantId).Result;
        }

        static async Task<string> GetAccessToken(string aadTenantId)
        {
            string accessToken = string.Empty;

            using (HttpClient httpClient = new HttpClient())
            {
                Uri uri = new Uri(Authority.Replace("{AadTenantId}", aadTenantId));
                Dictionary<string, string> requestBody = new Dictionary<string, string>
                {
                    {"grant_type", "client_credentials" },
                    {"client_id" , ClientId },
                    {"client_secret", ClientSecret },
                    {"scope", @"https://api.businesscentral.dynamics.com/.default" }
                };

                FormUrlEncodedContent request = new FormUrlEncodedContent(requestBody);

                try
                {
                    HttpResponseMessage response = await httpClient.PostAsync(uri, request);
                    string content = await response.Content.ReadAsStringAsync();

                    if (response.IsSuccessStatusCode)
                    {
                        JsonDocument document = JsonDocument.Parse(content);
                        accessToken = document.RootElement.GetProperty("access_token").GetString();
                        
                        Console.ForegroundColor = ConsoleColor.Green;
                        Console.WriteLine("Token acquired");
                        Console.ResetColor();
                    }
                    else
                    {
                        Console.ForegroundColor = ConsoleColor.Red;
                        Console.WriteLine($"Failed to retrieve access token: {response.StatusCode} {response.ReasonPhrase}");
                        Console.WriteLine($"Content: {content}");
                        Console.ResetColor();
                    }
                }
                catch (HttpRequestException ex)
                {
                    Console.ForegroundColor = ConsoleColor.Red;
                    Console.WriteLine($"Error occurred while retrieving access token");
                    Console.WriteLine($"{ex.Message}");
                    Console.ResetColor();
                }

            }

            return accessToken;
        }
    }
}

Let’s now look at a different way to retrieve the access token. A much better way in my opinion.

Retrieve access token with MSAL

Similar to what I wrote in the PowerShell example, we can use Microsoft Authentication Library for .NET (MSAL.NET) to do the hard work. You can find the MSAL repository at GitHub: https://github.com/AzureAD/microsoft-authentication-library-for-dotnet. To add this to the code, we need to get the NuGet package. I assume that you know how to do this in Visual Studio or VS Code.

Here is a code example to retrieve the access token with the MSAL.NET library:

using System;
using System.Threading.Tasks;
using Microsoft.Identity.Client;

namespace OAuthClientCredentialsDemo
{
    class Program
    {
        private const string ClientId = "3870c15c-5700-4704-8b1b-e020052cc860";
        private const string ClientSecret = "~FJRgS5q0YsAEefkW-_pA4ENJ_vIh-5RV9";
        private const string AadTenantId = "kauffmann.nl";
        private const string Authority = "https://login.microsoftonline.com/{AadTenantId}/oauth2/v2.0/token";

        static void Main(string[] args)
        {
            AuthenticationResult authResult = GetAccessToken(AadTenantId).Result;
        }

        static async Task<AuthenticationResult> GetAccessToken(string aadTenantId)
        {
            Uri uri = new Uri(Authority.Replace("{AadTenantId}", aadTenantId));

            IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(ClientId)
                .WithClientSecret(ClientSecret)
                .WithAuthority(uri)
                .Build();

            string[] scopes = new string[] { @"https://api.businesscentral.dynamics.com/.default" };
            AuthenticationResult result = null;
            try
            {
                result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
                
                Console.ForegroundColor = ConsoleColor.Green;
                Console.WriteLine("Token acquired");
                Console.ResetColor();
            }
            catch (MsalServiceException ex)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine($"Error occurred while retrieving access token");
                Console.WriteLine($"{ex.ErrorCode} {ex.Message}");
                Console.ResetColor();
            }

            return result;
        }
    }
}

The ConfidentialClientApplication provides a method AcquireTokenForClient which uses the client credentials flow to retrieve the access token. This method is documented here: https://docs.microsoft.com/en-us/dotnet/api/microsoft.identity.client.confidentialclientapplication.acquiretokenforclient. No need to compose a request, make a call, and process the returned JSON. It’s all included!

Using the access token

Now that we have the access token it’s time to use it. The code below has been modified to call Business Central API twice while reusing the access token. Please note that some variables have been added as well to compose the Business Central URL.

The most important part is how the access token is added to the request. That is done as an Authorization header on line 66: client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(“Bearer”, AuthResult.AccessToken);

using System;
using System.Threading.Tasks;
using System.Net.Http;
using System.Net.Http.Headers;
using Microsoft.Identity.Client;

namespace OAuthClientCredentialsDemo
{
    class Program
    {
        private const string ClientId = "3870c15c-5700-4704-8b1b-e020052cc860";
        private const string ClientSecret = "~FJRgS5q0YsAEefkW-_pA4ENJ_vIh-5RV9";
        private const string AadTenantId = "kauffmann.nl";
        private const string Authority = "https://login.microsoftonline.com/{AadTenantId}/oauth2/v2.0/token";
        private const string BCEnvironmentName = "sandbox";
        private const string BCCompanyId = "64d41503-fcd7-eb11-bb70-000d3a299fca";
        private const string BCBaseUrl = "https://api.businesscentral.dynamics.com/v2.0/{BCEnvironmentName}/api/v2.0/companies({BCCompanyId})";
        private static AuthenticationResult AuthResult = null;

        static void Main(string[] args)
        {
            string customers = CallBusinessCentralAPI(BCEnvironmentName, BCCompanyId, "customers").Result;
            string items = CallBusinessCentralAPI(BCEnvironmentName, BCCompanyId, "items").Result;
        }

        static async Task<AuthenticationResult> GetAccessToken(string aadTenantId)
        {
            Uri uri = new Uri(Authority.Replace("{AadTenantId}", aadTenantId));

            IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(ClientId)
                .WithClientSecret(ClientSecret)
                .WithAuthority(uri)
                .Build();

            string[] scopes = new string[] { @"https://api.businesscentral.dynamics.com/.default" };
            AuthenticationResult result = null;
            try
            {
                result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
                Console.ForegroundColor = ConsoleColor.Green;
                Console.WriteLine("Token acquired");
                Console.ResetColor();
            }
            catch (MsalServiceException ex)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine($"Error occurred while retrieving access token");
                Console.WriteLine($"{ex.ErrorCode} {ex.Message}");
                Console.ResetColor();
            }

            return result;
        }

        static async Task<string> CallBusinessCentralAPI(string bcEnvironmentName, string bcCompanyId, string resource)
        {
            string result = string.Empty;

            if ((AuthResult == null) || (AuthResult.ExpiresOn < DateTime.Now))
            {
                AuthResult = await GetAccessToken(AadTenantId);
            }

            using (HttpClient client = new HttpClient())
            {
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", AuthResult.AccessToken);
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

                Uri uri = new Uri(GetBCAPIUrl(bcEnvironmentName, bcCompanyId, resource));
                HttpResponseMessage response = await client.GetAsync(uri);

                if (response.IsSuccessStatusCode)
                {
                    result = await response.Content.ReadAsStringAsync();
                }
                else
                {
                    Console.ForegroundColor = ConsoleColor.Red;
                    Console.WriteLine($"Call to Business Central API failed: {response.StatusCode} {response.ReasonPhrase}");
                    string content = await response.Content.ReadAsStringAsync();
                    Console.WriteLine($"Content: {content}");
                    Console.ResetColor();
                }
            }

            return result;
        }

        private static string GetBCAPIUrl(string bcEnvironmentName, string bcCompanyId, string resource)
        {
            return BCBaseUrl.Replace("{BCEnvironmentName}", bcEnvironmentName).Replace("{BCCompanyId}", bcCompanyId) + "/" + resource;
        }
    }
}

Please keep in mind that the code examples are not optimized in many ways. It’s only meant to get you started with client credentials flow for Business Central in C#. The result from the API call to Business Central needs to be parsed as a JSON document or deserialized as an object in order to work with the data. That depends on your scenario of course. Let me know if you want to see some examples of that as well!

3 thoughts on “Service to service authentication in Business Central 18.3 – How to use in C#

  1. Hi,
    I followed the same steps and the Bearer token I got has these roles.
    “roles”: [
    “Automation.ReadWrite.All”,
    “app_access”,
    “API.ReadWrite.All”
    ],
    But still, if I access any BC Url, I am getting
    “”message”: “Access is denied to company ‘XXXXX’. ”
    Can you please share your idea on this?

  2. Hi,

    Thank you this blog.

    With given steps, I can get result from in-build web apis but for same code its returning bad request at the same time
    I can get results from postman.

    Is there something I am missing ?

    Thank you,
    Dev

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.