Dynamics 365 Authorisation Code Flow

Summary

In this flow, after the user successfully authenticates (through an interactive login prompt), an authorization code is issued by Azure AD. This code is then exchanged for an access token that can be used to make API calls.

Code Snippet

MSAL simplifies handling of this flow, managing login, token caching, token refreshing, and authentication.It manages PKCE (Proof Key for Code Exchange) for you, adding additional security to the Authorization Code Flow.

using Microsoft.Identity.Client;
using Microsoft.Xrm.Tooling.Connector;
using System;
using System.IO;
using System.Linq;  // LINQ for FirstOrDefault and Any
using System.Net.Http;
using System.Threading.Tasks;

internal class Program
{
    private static string clientId = "51f81489-12ee-4a9e-aaae-a2591f45987d"; // Pre-registered app ID
    private static string tenantId = "common";  // Use "common" for multi-tenant or specific tenant ID
    private static string[] scopes = new string[] { "https://eax.crm11.dynamics.com/.default" };
    private static string authority = $"https://login.microsoftonline.com/{tenantId}";
    private static string cacheFile = "msal_token_cache.bin";

    private static async Task Main(string[] args)
    {
        try
        {
            var pca = PublicClientApplicationBuilder.Create(clientId)
                .WithAuthority(authority)
                .WithRedirectUri("http://localhost")
                .Build();

            // Load cache from file (persistent cache)
            var tokenCache = pca.UserTokenCache;
            var cacheHelper = new TokenCacheHelper(cacheFile);
            tokenCache.SetBeforeAccess(cacheHelper.BeforeAccessNotification);
            tokenCache.SetAfterAccess(cacheHelper.AfterAccessNotification);

            AuthenticationResult result;
            var accounts = await pca.GetAccountsAsync();

            if (accounts.Any())  // Use .Any() instead of .Length
            {
                // Try to acquire token silently from cache
                try
                {
                    result = await pca.AcquireTokenSilent(scopes, accounts.FirstOrDefault()).ExecuteAsync(); // Use .FirstOrDefault()
                }
                catch (MsalUiRequiredException)
                {
                    // Fallback to interactive login if token is not found in cache
                    result = await pca.AcquireTokenInteractive(scopes).ExecuteAsync();
                }
            }
            else
            {
                // First time interactive login
                result = await pca.AcquireTokenInteractive(scopes).ExecuteAsync();
            }

            Console.WriteLine("Access Token: " + result.AccessToken);

            // Use the token for an API call (e.g., to retrieve the Contact SystemForm)
            using (var client = new HttpClient())
            {
                client.DefaultRequestHeaders.Authorization =
                    new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", result.AccessToken);

                // Query for Contact SystemForm by filtering the objecttypecode (Contact's
                // objecttypecode is usually 2)
                string query = "https://eax.crm11.dynamics.com/api/data/v9.0/systemforms?$filter=objecttypecode eq 'contact' and name eq 'Information'";

                var response = await client.GetAsync(query);
                var content = await response.Content.ReadAsStringAsync();

                Console.WriteLine("Response: " + content);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}

// Helper class to handle caching in file
public class TokenCacheHelper
{
    private readonly string CacheFilePath;

    public TokenCacheHelper(string filePath)
    {
        CacheFilePath = filePath;
    }

    public void BeforeAccessNotification(TokenCacheNotificationArgs args)
    {
        if (File.Exists(CacheFilePath))
        {
            var data = File.ReadAllBytes(CacheFilePath);
            args.TokenCache.DeserializeMsalV3(data);
        }
    }

    public void AfterAccessNotification(TokenCacheNotificationArgs args)
    {
        if (args.HasStateChanged)
        {
            var data = args.TokenCache.SerializeMsalV3();
            File.WriteAllBytes(CacheFilePath, data);
        }
    }
}