Implementing secure Microsoft Graph application clients in ASP.NET Core

The article looks at the different way a Microsoft Graph application client can be implemented and secured in an ASP.NET Core application or a .NET application. This type of client is intended for applications or application logic where no user is involved.

Code: https://github.com/damienbod/MicrosoftGraphAppToAppSecurity

Accessing Microsoft Graph can be initialized for app-to-app (application permissions) security in three different ways. The flows can only be used in a trusted host. The different implementation types are as follows:

  • Using Managed Identities
  • Using Azure SDK and Graph SDK directly with client credentials
  • Using Microsoft.Identity.Client and MSAL to acquire an access token which can be used directly against Microsoft Graph or using GraphServiceClient with the DelegateAuthenticationProvider class

Using Managed Identities

Using managed identities for the Azure deployments is the most secure of the three ways to implement this client. This is because no secret or certificates are shared and so cannot be abused and there is no need for secret rotation.

Setup

We use a web application deployed to an Azure App Service to setup the security. A managed identity is created for this Azure resource. If the Azure App Service is deleted, so is the managed identity and the assigned Graph roles. Only this Azure resource can use the managed identity.

Once the Azure resource is created, the Graph App roles can be assigned to the managed identity.

Powershell scripting

I created the Powershell script using a blog from Microsoft. This powershell script finds the managed identity and assigns the User.Read.All application permission to the managed identity.

$TenantID = "<your-tenant-id>"
$DisplayNameServicePrincpal ="<your-azure-app-registration-or-other-azure-resource>"
$GraphAppId = "00000003-0000-0000-c000-000000000000"
$PermissionName = "User.Read.All"

Connect-AzureAD -TenantId $TenantID

$sp = (Get-AzureADServicePrincipal -Filter "displayName eq '$DisplayNameServicePrincpal'")

Write-Host $sp

$GraphServicePrincipal = Get-AzureADServicePrincipal -Filter "appId eq '$GraphAppId'"

$AppRole = $GraphServicePrincipal.AppRoles | Where-Object {$_.Value -eq $PermissionName -and $_.AllowedMemberTypes -contains "Application"}

New-AzureAdServiceAppRoleAssignment -ObjectId $sp.ObjectId -PrincipalId $sp.ObjectId -ResourceId $GraphServicePrincipal.ObjectId -Id $AppRole.Id

This can be checked in the Azure portal using the Enterprise applications blade and filtering for managed identities.

The permissions contains the Graph User.Read.All application permission.

Implementing the client

The client is implemented using Azure.Identity and Graph SDK. We have two setups, one for the production and all other Azure deployments and one for development. The managed identity is used everywhere except the dev deployments and only this can be used. The local dev uses an Azure App registration with the client credentials flow. The GetGraphClientWithManagedIdentity method returns the GraphServiceClient Graph SDK client setup for the correct deployment. The correct ChainedTokenCredential is used to secure the client. It is important that only the correct managed identity for the exact resource can be used in production. No secret or certificates is required for this solution, the managed identity and Azure takes care of this. The GraphServiceClient is for the application and handles the HttpClient creation so the service is created as a singleton.

using Azure.Identity;
using Microsoft.Graph;

namespace GraphManagedIdentity;

public class GraphApplicationClientService
{
    private readonly IConfiguration _configuration;
    private readonly IHostEnvironment _environment;
    private GraphServiceClient? _graphServiceClient;

    public GraphApplicationClientService(IConfiguration configuration, IHostEnvironment environment)
    {
        _configuration = configuration;
        _environment = environment;
    }

    /// <summary>
    /// gets a singleton instance of the GraphServiceClient
    /// </summary>
    /// <returns></returns>
    public GraphServiceClient GetGraphClientWithManagedIdentityOrDevClient()
    {
        if (_graphServiceClient != null)
            return _graphServiceClient;

        string[] scopes = new[] { "https://graph.microsoft.com/.default" };

        var chainedTokenCredential = GetChainedTokenCredentials();
        _graphServiceClient = new GraphServiceClient(chainedTokenCredential, scopes);

        return _graphServiceClient;
    }

    private ChainedTokenCredential GetChainedTokenCredentials()
    {
        if (!_environment.IsDevelopment())
        {
            return new ChainedTokenCredential(new ManagedIdentityCredential());
        }
        else // dev env
        {
            var tenantId = _configuration["AzureAd:TenantId"];
            var clientId = _configuration.GetValue<string>("AzureAd:ClientId");
            var clientSecret = _configuration.GetValue<string>("AzureAd:ClientSecret");

            var options = new TokenCredentialOptions
            {
                AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
            };

            // https://docs.microsoft.com/dotnet/api/azure.identity.clientsecretcredential
            var devClientSecretCredential = new ClientSecretCredential(
                tenantId, clientId, clientSecret, options);

            var chainedTokenCredential = new ChainedTokenCredential(devClientSecretCredential);

            return chainedTokenCredential;
        }
    }
}

The service is added to the IoC and can be used anywhere in the application. Once deployed, the managed identity is used, otherwise the dev setup runs.

builder.Services.AddSingleton<GraphApplicationClientService>();
builder.Services.AddScoped<AadGraphSdkApplicationClient>();

I use it in a service then:

using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using Microsoft.Graph;
using System.Security.Cryptography.X509Certificates;

namespace GraphClientCrendentials;

public class AadGraphSdkApplicationClient
{
    private readonly IConfiguration _configuration;
    private readonly GraphApplicationClientService _graphService;

    public AadGraphSdkApplicationClient(IConfiguration configuration, GraphApplicationClientService graphService)
    {
        _configuration = configuration;
        _graphService = graphService;
    }

    public async Task<int> GetUsersAsync()
    {
        var graphServiceClient = _graphService.GetGraphClientWithClientSecretCredential();

        IGraphServiceUsersCollectionPage users = await graphServiceClient.Users
            .Request()
            .GetAsync();

        return users.Count;
    }
}

Dev setup

An Azure App registration is used to implement the OAuth client credentials flow and uses the Graph SDK client in development. The Graph application is added to the single tenant Azure App registration. An enterprise application is created from this.

The ChainedTokenCredential uses the app.settings and the user secrets to configure the client. The client uses the OAuth client credentials flow to acquire an access token. I normally use secrets for development for simplicity but if more security is required, a certificate can be used and the secret/certificate can be used directly from an Azure KeyVault.

  "AzureAd": {
    "TenantId": "7ff95b15-dc21-4ba6-bc92-824856578fc1",
    "ClientId": "3606b25d-f670-4bab-ab70-437460143d89"
    //"ClientSecret": "add secret to the user secrets"
    //"CertificateName": "[Or instead of client secret: Enter here the name of a certificate (from the user cert store) as registered with your application]",
    //"Certificate": {
    //  "SourceType": "KeyVault",
    //  "KeyVaultUrl": "<VaultUri>",
    //  "KeyVaultCertificateName": "<CertificateName>"
    //}
  },

Using Azure SDK and Graph SDK directly

A Microsoft Graph client can be setup to to use the client credentials flow to initialize the Graph SDK GraphServiceClient. This is a good way of creating the OAuth client credentials flow if it is used outside the Azure tenant. It is recommended to use a certificate and this is normally stored in an Azure Key Vault. This uses the OAuth client credentials flow and uses the client assertions to acquire a new access token.

The flow can be setup to use a secret:

private GraphServiceClient GetGraphClientWithClientSecretCredential()
{
	string[] scopes = new[] { "https://graph.microsoft.com/.default" };
	var tenantId = _configuration["AzureAd:TenantId"];

	// Values from app registration
	var clientId = _configuration.GetValue<string>("AzureAd:ClientId");
	var clientSecret = _configuration.GetValue<string>("AzureAd:ClientSecret");

	var options = new TokenCredentialOptions
	{
		AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
	};

	// https://docs.microsoft.com/dotnet/api/azure.identity.clientsecretcredential
	var clientSecretCredential = new ClientSecretCredential(
		tenantId, clientId, clientSecret, options);

	return new GraphServiceClient(clientSecretCredential, scopes);
}

Or setup to use a certificate:

private async Task<GraphServiceClient> GetGraphClientWithClientCertificateCredentialAsync()
{
	string[] scopes = new[] { "https://graph.microsoft.com/.default" };
	var tenantId = _configuration["AzureAd:TenantId"];

	var options = new TokenCredentialOptions
	{
		AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
	};

	// Values from app registration
	var clientId = _configuration.GetValue<string>("AzureAd:ClientId");

	var certififacte = await GetCertificateAsync();
	var clientCertificateCredential = new ClientCertificateCredential(
		tenantId, clientId, certififacte, options);

	// var clientCertificatePath = _configuration.GetValue<string>("AzureAd:CertificateName");
	// https://learn.microsoft.com/en-us/dotnet/api/azure.identity.clientcertificatecredential?view=azure-dotnet
	// var clientCertificateCredential = new ClientCertificateCredential(
	//    tenantId, clientId, clientCertificatePath, options);

	return new GraphServiceClient(clientCertificateCredential, scopes);
}

private async Task<X509Certificate2> GetCertificateAsync()
{
	var identifier = _configuration["AzureAd:ClientCertificates:0:KeyVaultCertificateName"];

	if (identifier == null)
		throw new ArgumentNullException(nameof(identifier));

	var vaultBaseUrl = _configuration["AzureAd:ClientCertificates:0:KeyVaultUrl"];
	if(vaultBaseUrl == null)
		throw new ArgumentNullException(nameof(vaultBaseUrl));

	var secretClient = new SecretClient(vaultUri: new Uri(vaultBaseUrl), credential: new DefaultAzureCredential());

	// Create a new secret using the secret client.
	var secretName = identifier;
	//var secretVersion = "";
	KeyVaultSecret secret = await secretClient.GetSecretAsync(secretName);

	var privateKeyBytes = Convert.FromBase64String(secret.Value);

	var certificateWithPrivateKey = new X509Certificate2(privateKeyBytes,
		string.Empty, X509KeyStorageFlags.MachineKeySet);

	return certificateWithPrivateKey;
}

I usually use a secret for development and a certificate for production.

Using Microsoft.Identity.Client and MSAL

A third way of implementing the Graph client is to use Microsoft.Identity.Client or Microsoft.Identity.Web. This uses the ConfidentialClientApplicationBuilder to create a new IConfidentialClientApplication instance and can use a secret or a certificate to acquire the access token.

Microsoft.Identity.Client with a secret:

 var app = ConfidentialClientApplicationBuilder.Create(config.ClientId)
    .WithClientSecret(config.ClientSecret)
    .WithAuthority(new Uri(config.Authority))
    .Build();

app.AddInMemoryTokenCache();

or with a certificate and client assertions:

 var app = ConfidentialClientApplicationBuilder.Create(config.ClientId)
    .WithCertificate(certificate)
    .WithAuthority(new Uri(config.Authority))
    .Build(); 
  
app.AddInMemoryTokenCache();

The GraphServiceClient can be created using the DelegateAuthenticationProvider. As I understand you should avoid using the DelegateAuthenticationProvider if possible.

GraphServiceClient graphServiceClient =
    new GraphServiceClient("https://graph.microsoft.com/V1.0/", 
        new DelegateAuthenticationProvider(async (requestMessage) =>
        {
            // Retrieve an access token for Microsoft Graph (gets a fresh token if needed).
            AuthenticationResult result = await app.AcquireTokenForClient(scopes)
                .ExecuteAsync();

            // Add the access token in the Authorization header of the API request.
            requestMessage.Headers.Authorization =
                new AuthenticationHeaderValue("Bearer", result.AccessToken);
        }));
}

Notes

There are three different ways of creating Microsoft Graph application clients and it is sometimes hard to understand when you should use which. This is not for the delegated clients. In an ASP.NET Core application you would use Microsoft.Identity.Web for a delegated client which then uses Microsoft Graph on behalf of the user. System assigned managed identities do not require managing secrets or certificates but can only be used in the same tenant. The client credentials flow can be used from anywhere. Microsoft recommends using certificates when using the client credentials flow.

Links

https://learn.microsoft.com/en-us/azure/app-service/scenario-secure-app-access-microsoft-graph-as-app?tabs=azure-powershell

https://learn.microsoft.com/en-us/azure/active-directory/develop/sample-v2-code#service–daemon

https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2/tree/master/1-Call-MSGraph

https://oceanleaf.ch/azure-managed-identity/

https://learningbydoing.cloud/blog/stop-using-client-secrets-start-using-managed-identities/

https://github.com/Azure/azure-sdk-for-net

https://learn.microsoft.com/en-us/dotnet/api/azure.identity.environmentcredential?view=azure-dotnet

https://learn.microsoft.com/en-us/graph/sdks/choose-authentication-providers?tabs=CS

4 comments

  1. […] Implementing secure Microsoft Graph application clients in ASP.NET Core [#.NET #.NET Core #App Service #ASP.NET Core #Azure #AzureAD #client assertions #Client credentials #managed identity #MSAL #OAuth2 #service principal] […]

  2. […] Implementing secure Microsoft Graph application clients in ASP.NET Core – Damien Bowden […]

  3. […] Implementing secure Microsoft Graph application clients in ASP.NET Core (Damien Bowden) […]

  4. […] Implementing secure Microsoft Graph application clients in ASP.NET Core […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

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

%d bloggers like this: