Implement Microsoft Entra ID Client credentials flow using Client Certificates for service APIs

This post shows how to implement an Microsoft Entra ID client credential flows to access an API for a service-to-service connection. No user is involved in this flow. A client certificate (Private Key JWT authentication) is used to get the access token and the token is used to access the API which is then used and validated in the API. Azure Key Vault is used to create and provide the client certificate.

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

Posts in this series

History

  • 2023-11-28 Updated to .NET 8
  • 2021-01-19 Updated packages, using Azure.Extensions.AspNetCore.Configuration.Secrets

Create a client certificate in Azure Key Vault

A self signed certificate with a key size of at least 2048 and key type RSA is used to validate the client requesting the access token. In your Azure Vault create a new certificate.

Download the .cer file which contains the public key. This will be uploaded to the Azure App Registration.

Setup the Azure App Registration for the Service API

A new Azure App Registration can be created for the Service API. This API will use a client certificate to request access tokens. The public key of the certificate needs to be added to the registration. In the Certificates & Secrets, upload the .cer file which was downloaded from the Key Vault.

No user is involved in the client credentials flow. In Microsoft Entra ID, scopes cannot be used because consent is required to use scopes (Azure specific). Two roles are added to the access token for the application access and these roles can then be validated in the API. Open the Manifest and update the “appRoles” to include the required roles. The allowedMemberTypes should be Application.

Every time an access token is requested for the API, the roles will be added to the token. The “clientId/.default” scope is used to request the access token, ie no consent and all claims are added. The required claims can be added using the API permissions.

In the API permissions/Add a permission/My APIs select application and then the API Azure App Registration and add the roles which where created in the Manifest.

The Azure App Registration and the Key Vault are now ready so that client certificates can be used to request an access token which can be used to get data from the API.

Using the Azure Key Vault certificate

Microsoft.Identity.Web is used to implement the code along with Azure SDK to access the Key Vault.

Managed identities are used to access the Key Vault from the application. The Key Vault needs to be configured for the identities in the access policies. When running from the local dev environment in Visual Studio, the logged in user needs to have certificate access to the Key Vault. The deployed Azure App Service would also need this (if deploying to Azure App Services). The Nuget package Azure.Extensions.AspNetCore.Configuration.Secrets is used for the Key Vault certificate access. The Microsoft.Azure.Services.AppAuthentication nuget package is used for the Azure credentials.

// Use Key Vault to get certificate 
var azureServiceTokenProvider = new AzureServiceTokenProvider(); 
// Get the certificate from Key Vault 
var identifier = _configuration["CallApi:ClientCertificates:0:KeyVaultCertificateName"]; 
var cert = await GetCertificateAsync(identifier);

A X509Certificate2 can then be created from the Azure SDK CertificateVersionBundle returned from the GetCertificateAsync method.


private async Task GetCertificateAsync(string? identitifier)
{
var vaultBaseUrl = _configuration[“CallApi:ClientCertificates:0:KeyVaultUrl”];
vaultBaseUrl ??= “https://damienbod.vault.azure.net”;

    var tenantId = _configuration["CallApi:TenantId"];
    var clientId = _configuration["CallApi:ClientId"];
    var clientSecretKeyVaultAccess = _configuration["ClientSecretKeyVaultAccess"];

    var secretClient = new SecretClient(vaultUri: new Uri(vaultBaseUrl),
        credential: new ClientSecretCredential(tenantId, clientId, clientSecretKeyVaultAccess));

    // Create a new secret using the secret client.
    var secretName = identitifier;
    //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;
}

Implement the API client using IConfidentialClientApplication and certificates

The IConfidentialClientApplication interface is used to setup the Microsoft Entra ID client credentials flow. This is part of the Microsoft.Identity.Client namespace. The certificate from Key Vault is used to create the Access token request. The …/.default scope must be used for this flow in Microsoft Entra ID. The AcquireTokenForClient is then used to send the request for the access token.

https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#second-case-access-token-request-with-a-certificate

// client credentials flows, get access token
IConfidentialClientApplication app = ConfidentialClientApplicationBuilder
.Create(_configuration["CallApi:ClientId"])
	.WithAuthority(new Uri(authority))
	.WithHttpClientFactory(new MsalHttpClientFactoryLogger(_logger))
	.WithCertificate(cert)
	.WithLogging(MyLoggingMethod, Microsoft.Identity.Client.LogLevel.Verbose,
		enablePiiLogging: true, enableDefaultPlatformLogging: true)
	.Build();

The access token returned from the AcquireTokenForClient method can then be used to access the API. This is added as a HTTP header.

client.BaseAddress = new Uri(_configuration["CallApi:ApiBaseAddress"]);
client.DefaultRequestHeaders.Authorization 
	= new AuthenticationHeaderValue("Bearer", accessToken.AccessToken);
client.DefaultRequestHeaders.Accept
	.Add(new MediaTypeWithQualityHeaderValue("application/json"));

// use access token and get payload
var response = await client.GetAsync("weatherforecast");
if (response.IsSuccessStatusCode)
{
	var responseContent = await response.Content.ReadAsStringAsync();
	var data = System.Text.Json
		.JsonSerializer
		.Deserialize<IEnumerable<WeatherForecast>>(responseContent);

	return data;
}

The app.settings contains the configuration for the Service API and the Azure App registration specifics. The ScopeForAccessToken uses the api://–clientid–/.default as this is required for Microsoft Entra ID client credentials flow. The ClientCertificates contains the key vault settings as defined in the Microsoft.Identity.Web docs.

"CallApi": {
    "ScopeForAccessToken": "api://b178f3a5-7588-492a-924f-72d7887b7e48/.default",
    "ApiBaseAddress": "https://localhost:44390",
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "damienbodhotmail.onmicrosoft.com",
    "TenantId": "7ff95b15-dc21-4ba6-bc92-824856578fc1",
    "ClientId": "b178f3a5-7588-492a-924f-72d7887b7e48",
    "ClientCertificates": [
      {
        "SourceType": "KeyVault",
        "KeyVaultUrl": "https://damienbod.vault.azure.net",
        "KeyVaultCertificateName": "ServiceApiCert"
      }
    ]
  },

Logging the client calls

A delegate method can be used to add your own specific logging of the IConfidentialClientApplication implementation. MyLoggingMethod implements this as shown in the docs.

void MyLoggingMethod(Microsoft.Identity.Client.LogLevel level, string message, bool containsPii) 
{ 
    _logger.LogInformation("MSAL {level} {containsPii} {message}"); 
}

This can then be used by implementing the WithLogging method. In production deployments, the demo configurations should be changed.

Securing the API

IConfidentialClientApplication app = ConfidentialClientApplicationBuilder 
  .Create(_configuration["CallApi:ClientId"]) 
  .WithAuthority(new Uri(authority))
  .WithCertificate(cert)
  .WithLogging(MyLoggingMethod, Microsoft.Identity.Client.LogLevel.Verbose, enablePiiLogging: true, enableDefaultPlatformLogging: true) 
  .Build();

The API now needs to enforce the security and validate the access token. This API can only be used by services and client certificate authentication is required. The AddMicrosoftIdentityWebApiAuthentication extension method adds the Microsoft.Identity.Web code configuration. This is configured to use and check the client certificate. The azpacr claim and the azp claim are validated in the AddAuthorization method. The azpacr value must be two, meaning a client certificate was used for authentication. The required roles are also validated using an authorization policy.

public static WebApplication ConfigureServices(
	this WebApplicationBuilder builder)
{
	var services = builder.Services;
	var configuration = builder.Configuration;

	services.AddTransient<ConfidentialClientApiService>();
	services.AddTransient<ClientAssertionsApiService>();
	services.AddHttpClient();
	services.AddOptions();

	services.AddMicrosoftIdentityWebAppAuthentication(configuration);

	services.AddRazorPages().AddMvcOptions(options =>
	{
		var policy = new AuthorizationPolicyBuilder()
			.RequireAuthenticatedUser()
			.Build();
		options.Filters.Add(new AuthorizeFilter(policy));
	}).AddMicrosoftIdentityUI();
	return builder.Build();
}

The configuration for the API contains the Azure App Registration specifics as well as the certificate details to get the certificate from the Key Vault.

In the Controller for the API, the ValidateAccessTokenPolicy is applied.

[Authorize(Policy = "ValidateAccessTokenPolicy")] 
[ApiController] 
[Route("[controller]")] 
public class WeatherForecastController : ControllerBase

The HasServiceApiRoleHandler implements the HasServiceApiRoleRequirement requirement. This checks if the required role is present.

public class HasServiceApiRoleHandler : AuthorizationHandler<HasServiceApiRoleRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HasServiceApiRoleRequirement requirement)
    {
        if (context == null)
            throw new ArgumentNullException(nameof(context));
        if (requirement == null)
            throw new ArgumentNullException(nameof(requirement));

        var roleClaims = context.User.Claims.Where(t => t.Type == "roles");

        if (roleClaims != null && HasServiceApiRole(roleClaims))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }

    private static bool HasServiceApiRole(IEnumerable<Claim> roleClaims)
    {
        // we could also validate the "access_as_application" scope
        foreach (var role in roleClaims)
        {
            if ("service-api" == role.Value)
            {
                return true;
            }
        }

        return false;
    }
}

Using a client certificate to identify an application client calling an API can be very useful. If you do not implement both the client and the API of a confidential client, using certificates instead of secrets can be very useful, as you do not have to share a secret. The client can provide a public key, and the server can validate this. If you control both the client and the API, then both APIs could use the same secret from the same Key Vault. Private Key JWT authentication for other flow types and other API types such as access_as_user or OBO flows is also supported using Microsoft.Identity.Web.

Links

https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#second-case-access-token-request-with-a-certificate

https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/Client-credential-flows

https://tools.ietf.org/html/rfc7523

https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication

https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens

https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/Client-Assertions

https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow

https://github.com/AzureAD/microsoft-identity-web/wiki/Using-certificates#describing-client-certificates-to-use-by-configuration

API Security with OAuth2 and OpenID Connect in Depth with Kevin Dockx, August 2020

https://www.scottbrady91.com/OAuth/Removing-Shared-Secrets-for-OAuth-Client-Authentication

https://github.com/KevinDockx/ApiSecurityInDepth

https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki

Authentication and the Azure SDK

6 comments

  1. […] Implement Azure AD Client credentials flow using Client Certificates for service APIs – Damien Bowden […]

  2. Michele Buzzoni · · Reply

    there’s a way to do this in react?

  3. I have used this information to perform invitations using a certificate. Very useful information. Thank you.

  4. […] /https://damienbod.com/2020/10/01/implement-azure-ad-client-credentials-flow-using-client-certificate… […]

Leave a comment

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