Dynamics\CDS client + Azure Functions + Managed Identities

Dynamics\CDS client + Azure Functions + Managed Identities

I'm catching up on Azure Functions in context of Dynamics 365 / Power Apps CDS. There are some binding redirect issues with V1 runtime, but overall I like them.

Looking on code in commercial projects but also on GitHub I noticed that there is lack of understanding how to deal with authentication to CDS and storing credentials. In worst case scenario full connection string is stored in function's application settings and uses someones (e.g. developer) personal user for authentication and this user has system administrator role. In better cases connection string is stored in Key Vault or any other service for storing secrets. But there something better to forget about usernames and password - Managed Identities. It allows to assign function or other service an identity and easily obtain tokens for accessing other resources. I won't go into details of how managed identity works on its own. Instead I'll focus on making it work together with functions and CDS.

Create managed identity

We will need to create user assigned managed identity and write down Client ID. User assigned MI may be reused between function apps.

Add new application user to CDS

In Application Users view click new, give it meaningful username and use Client ID of managed identity to set Application ID. After saving assign this user some roles. In a perfect world it should be specific role for service account with minimal permissions.

Assign user managed identity to Azure Function

Go Function App in Azure portal and then to Identity settings. Add previously create managed identity.

Use MSAL to provide token for CrmServiceClient/CdsServiceClient

MSAL stands for Microsoft Authentication Library and it successor of ADAL which is deprecated now. MSAL will be used to obtain valid token for CDS service for our managed identity. I'll show two examples: one for old CrmServiceClien and full .NET Framework and one for CdsService client for dotnet core, which is in alpha stage right now.

CrmServiceClient

We need to implement IOverrideAuthHookWrapper interface. It's pretty straightforward. It instantiates ManagedIdentityCredential class from MSAL which takes our managed identity's client ID. GetAuthToken method receives full organization service URL from CrmServiceClient, but for scope we need only scheme and protocol (e.g. https://org.crm4.dynamics.com).

class AzureFuncOverrideAuthHookWrapper : IOverrideAuthHookWrapper
{
    private readonly ManagedIdentityCredential managedIdentityCredential;

    public AzureFuncOverrideAuthHookWrapper(string clientId)
    {
        this.managedIdentityCredential = new ManagedIdentityCredential(clientId);
    }

    public string GetAuthToken(Uri connectedUri)
    {
        var properScope = connectedUri.GetComponents(UriComponents.SchemeAndServer, UriFormat.UriEscaped);

        var acessToken = managedIdentityCredential.GetToken(new TokenRequestContext(new[] { properScope }));

        return acessToken.Token;
    }
}

Next we need to configure client to use it.

CrmServiceClient.AuthOverrideHook = new AzureFuncOverrideAuthHookWrapper(clientId);

Basically that's it. We can create CrmServiceClient instance without specifying any credentials explicitly.

log.Info("Creating CrmServiceClient.");
var crmServiceClient = new CrmServiceClient(new Uri(cdsUri), true);

log.Info("Executing WhoAmI request.");
var whoAmIResponse = (WhoAmIResponse)crmServiceClient.Execute(new WhoAmIRequest());

CdsServiceClient

It's a bit different, but the general idea is the same. CdsServiceClient takes token provider function as constructor argument, which is then called to to get token. I used private static method to implement it.

private static async Task<string> GetTokenAsync(string instanceUri)
{
    var managedIdentityCredential = new ManagedIdentityCredential(clientId);

    var properScope = new Uri(instanceUri).GetComponents(UriComponents.SchemeAndServer, UriFormat.UriEscaped);
    var acessToken = await managedIdentityCredential.GetTokenAsync(new TokenRequestContext(new[] { properScope }));

    return acessToken.Token;
}

Instantiating and using CDS client.

log.LogInformation("Creating CdsServiceClient.");
var cdsServiceClient = new CdsServiceClient(new Uri(cdsUri), GetTokenAsync, true);

log.LogInformation("Executing WhoAmI request.");
var whoAmIResponse = (WhoAmIResponse)cdsServiceClient.Execute(new WhoAmIRequest());

Advantages of this approach is that we don't need store credentials anywhere. We need to put client ID in settings, but this is not security critical information. We have special service user in CDS, which doesn't require any additional licences. Tokens are cached by managed identities service, so theoretically it should be a bit faster than doing external (for Function App) authentication.

Solution is available on GitHub: https://github.com/sergeytunnik/azure-func-cds-mi

Show Comments