This post shows how to add debug logging to the Microsoft.Identity.Client MSAL client which is used to implement an OAuth2 client credentials flow using a client assertion. The client uses the MSAL nuget package. PII logging was activated and the HttpClient was replaced to log all HTTP requests and responses from the MSAL package.
Code: ConfidentialClientCredentialsCertificate
The Microsoft.Identity.Client is used to implement the client credentials flow. A known certificate is used to implement the client authentication using a client assertion in the token request. The IConfidentialClientApplication uses a standard client implementation with two extra extension methods, one to add the PII logging and a second to replace the HttpClient used for the MSAL requests and responses. The certificate is read from Azure Vault using the Azure SDK and managed identities on a deployed instance.
// 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);
var scope = _configuration["CallApi:ScopeForAccessToken"];
var authority = $"{_configuration["CallApi:Instance"]}{_configuration["CallApi:TenantId"]}";
// 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();
var accessToken = await app.AcquireTokenForClient(new[] { scope }).ExecuteAsync();
The GetCertificateAsync loads the certificate from an Azure key vault. This is slow in local development and you could replace this with an host installed certificate for development.
private async Task<X509Certificate2> GetCertificateAsync(string identitifier)
{
var vaultBaseUrl = _configuration["CallApi:ClientCertificates:0:KeyVaultUrl"];
var secretClient = new SecretClient(vaultUri: new Uri(vaultBaseUrl), credential: new DefaultAzureCredential());
// 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;
}
WithLogging
The WithLogging is used to add the PII logging and to change the log level. You should never do this on a production deployment and all the PII data would get logged and saved to the logging persistence. This includes access tokens from all users or application clients using the client package. This is great for development, if you need to see why an access token does not work with an API and check the claims inside the access token. The MyLoggingMethod method is used in the WithLogging extension method.
void MyLoggingMethod(Microsoft.Identity.Client.LogLevel level, string message, bool containsPii)
{
_logger.LogInformation("MSAL {level} {containsPii} {message}", level, containsPii, message);
}
The WithLogging can be used as follows:
.WithLogging(MyLoggingMethod, Microsoft.Identity.Client.LogLevel.Verbose,
enablePiiLogging: true, enableDefaultPlatformLogging: true)
Now all logs will be logged for this client.
WithHttpClientFactory
I would also like to see how the MSAL package implements the OAuth client credentials flow and see what is sent in the requests and the corresponding responses. I replaced the MsalHttpClientFactory with my MsalHttpClientFactoryLogger inplementation and logged everything.
.WithHttpClientFactory(new MsalHttpClientFactoryLogger(_logger))
To implement this, I used an implementation of the DelegatingHandler. This logs all and the full HTTP requests and responses for the MSAL client.
using Microsoft.Extensions.Logging;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ServiceApi.HttpLogger;
public class MsalLoggingHandler : DelegatingHandler
{
private ILogger _logger;
public MsalLoggingHandler(HttpMessageHandler innerHandler, ILogger logger)
: base(innerHandler)
{
_logger = logger;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var builder = new StringBuilder();
builder.AppendLine("MSAL Request: {request}");
builder.AppendLine(request.ToString());
if (request.Content != null)
{
builder.AppendLine();
builder.AppendLine(await request.Content.ReadAsStringAsync());
}
HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
builder.AppendLine();
builder.AppendLine("MSAL Response: {response}");
builder.AppendLine(response.ToString());
if (response.Content != null)
{
builder.AppendLine();
builder.AppendLine(await response.Content.ReadAsStringAsync());
}
_logger.LogDebug(builder.ToString());
return response;
}
}
The message handler is used in the IMsalHttpClientFactory implementation. I pass the default ILogger into the method and use this to log. In the source code, Serilog is used.
Do not use this in production as everything gets logged and persisted to the server. This is good to see how the client is implemented.
using Microsoft.Extensions.Logging;
using Microsoft.Identity.Client;
using System.Net.Http;
namespace ServiceApi.HttpLogger;
public class MsalHttpClientFactoryLogger : IMsalHttpClientFactory
{
private static HttpClient _httpClient;
public MsalHttpClientFactoryLogger(ILogger logger)
{
if (_httpClient == null)
{
_httpClient = new HttpClient(new MsalLoggingHandler(new HttpClientHandler(), logger));
}
}
public HttpClient GetHttpClient()
{
return _httpClient;
}
}
OAuth client credentials with client assertion
I ran the extra logging then with an OAuth2 client credentials flow using client authentication client assertions.
The discovery endpoint is called first from the MSAL client for the Azure App registration used to configure the client. This returns all the well known endpoints.
MSAL Request: {request}
Method: GET, RequestUri: 'https://login.microsoftonline.com/common/discovery/instance?api-version=1.1&authorization_endpoint=https%3A%2F%2Flogin.microsoftonline.com%2F7ff95b15-dc21-4ba6-bc92-824856578fc1%2Foauth2%2Fv2.0%2Fauthorize'
MSAL Response: {response}
{
"tenant_discovery_endpoint": "https://login.microsoftonline.com/7ff95b15-dc21-4ba6-bc92-824856578fc1/v2.0/.well-known/openid-configuration",
The token is requested using a client_assertion parameter with a signed JWT token using the client certificate created for this application. Only this client knows the private key and the public key was uploaded to the Azure App registration. See the following link for the spec details:
https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
If the JWT has the correct claims and is signed by the correct certificate, an access token is returned for the application confidential client. This request can only be sent and created for an application in possession of the private certificate key. This is more secure then the same OAuth flow using client secrets as any client can send a token request once the secret is shared. Using the client assertion and a signed JWT request, we can achieve a better client authentication. The request cannot be used twice and the correct implementation enforces this by validating the jti claim in the signed JWT. The token must only be used once.
2022-08-03 20:09:40.364 +02:00 [DBG] MSAL Request: {request}
Method: POST, RequestUri: 'https://login.microsoftonline.com/7ff95b15-dc21-4ba6-bc92-824856578fc1/oauth2/v2.0/token', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
Content-Type: application/x-www-form-urlencoded
}
client_id=b178f3a5-7588-492a-924f-72d7887b7e48
&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer
&client_assertion=eyJhbGciOiJSUzI1...
&scope=api%3A%2F%2Fb178f3a5-7588-492a-924f-72d7887b7e48%2F.default
&grant_type=client_credentials
MSAL Response: {response}
StatusCode: 200, ReasonPhrase: 'OK', Version: 1.1, Content: System.Net.Http.HttpConnectionResponseContent, Headers:
{
Content-Type: application/json; charset=utf-8
}
{
"token_type":"Bearer",
"expires_in":3599,"ext_expires_in":3599,
"access_token":"eyJ0eXAiOiJKV..."
}
The signed JWT client assertion contains the following claims. The OpenID Connect specification defines the required claims and further optional claims can be included in the request JWT as required. Microsoft.Identity.Client supports adding customs claims if required.

By adding PII logs and logging all requests and responses from the MSAL client, it is possible to see exactly how the client was implemented and works without having to reverse engineer the code. Do not use this in production!
For clients without a user, you should implement the client credentials flow using certificates whenever possible, as this is a more secure and improved authentication compared to the same flow using client secrets.
Links
https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-net-client-assertions
https://docs.microsoft.com/en-us/azure/architecture/multitenant-identity/client-certificate
https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
[…] Debug Logging Microsoft.Identity.Client and the MSAL OAuth client credentials flow (Damien Bowden) […]