Using MSAL.Net to perform the client credentials flow with a certificate instead of a client secret in a .NetCore console appliction.

The sample files for this post can be found in this GitHub repository: https://github.com/RayGHeld/NetCore_ClientCredentials_withCert

There is also a Powershell script there to create a certificate for this sample.

You can perform the OAuth2 client_credentials grant flow to sign in as an application for your automated type of services. This flow does not require an interactive user to authenticate and should only be run in secure environments. There are 2 methods to handle securing the authentication portion: 1) use a client secret and 2) use a certificate. This blog post will show you how to authenticate using this flow in a .NetCore console application and a certificate.

The flow that is being followed is documented here. To begin the process, you will need a certificate and for this post, I will be using a self signed certificate using Powershell. I borrowed the Powershell example from this blog post and made modifications for this post so that the certificate that is created lines up with code since I am demonstrating multiple ways to get the certificate from the certificate store.

The first step is to create the certificate. You can find the Powershell script in the GitHub link at the start of this post. I have masked my tenant id and client id. Please remember to use your own. Also, note the folder location that the cert is created in ( you can change that value as well ) but you will need that in the .NetCore app.

# Create self-signed certificate and export pfx and cer files.  You will need to run the script in PowerShell Admin window to create this self-signed certificate

# please be aware that running this script will create a new cert each time in your certificate store.  
# If you need to rerun it, I recommend deleting the previous certs first in the store.
# Duplicates are only really an issue if you are using the GetCertByName method

$tenant_id = 'xxxx' # tenant id is used for the cert name / subject name and matches the .netcore code sample that goes with this script
$client_id = 'xxxx' # client id is used for the cert name / subject name and cert password and matches the .netcore code sample that goes with this script
# Change the following file path to your own
$FilePath = 'C:\Users\myuser\OneDrive - Microsoft\Documents\Certs\'
$StoreLocation = 'CurrentUser' # be aware that LocalMachine requires elevated privileges
$expirationYears = 1

$SubjectName = $tenant_id + '.' + $client_id
$cert_password = $client_id

$pfxFileName = $SubjectName + '.pfx'
$cerFileName = $SubjectName + '.cer'

$PfxFilePath = $FilePath + $pfxFileName
$CerFilePath = $FilePath + $cerFileName

$CertBeginDate = Get-Date
$CertExpiryDate = $CertBeginDate.AddYears($expirationYears)
$SecStringPw = ConvertTo-SecureString -String $cert_password -Force -AsPlainText 
$Cert = New-SelfSignedCertificate -DnsName $SubjectName -CertStoreLocation "cert:\$StoreLocation\My" -NotBefore $CertBeginDate -NotAfter $CertExpiryDate -KeySpec Signature
Export-PfxCertificate -cert $Cert -FilePath $PFXFilePath -Password $SecStringPw 
Export-Certificate -cert $Cert -FilePath $CerFilePath 

Once the certificate is created, you can find it in your certificate store on the computer the script was ran from. To get to the certificate store on the computer, I simply did a search in the tool bar search for “Certificate” and then used the Manage user certificates link that appeared.

Once the certificate is created, you can upload that to an app registration in the Azure portal.

For the .netcore application, you will need to make similar changes to the client id, tenant id and also the folder path. Also, you can enter the thumbprint here as well since there is a method to search for the certificate by thumbprint ( which is actually the best way of doing this ).

        static String accessToken = string.Empty;
        static String client_id = "xxxx";
        static String cert_password = client_id;
        static String tenant_id = "xxxx";
        static String authority = $"https://login.microsoftonline.com/{tenant_id}";
        static List<String> scopes = new List<String>() { $"https://graph.microsoft.com/.default" };
        static String thumbprint = "xxxxxx";

There are 3 methods that will search for the certificate. I have 2 of them commented out ( you can only use one method here so choose one, comment the other 2 out when testing ) and they are part of the confidential client builder class — see the method called GetToken.

            IConfidentialClientApplication ica = ConfidentialClientApplicationBuilder.Create(client_id)
                .WithCertificate(GetCert(@"C:\Users\myuser\OneDrive - Microsoft\Documents\Certs\"))
                //.WithCertificate(GetCertByThumbprint( StoreLocation.CurrentUser, thumbprint ) )
                //.WithCertificate( GetCertByName( StoreLocation.CurrentUser, $"{tenant_id}.{client_id}" ) )
                .WithTenantId(tenant_id)
                .WithAuthority(authority)
                .Build();

And, that is it. Your application will perform the client credentials flow using the certificate you created. This demo app does not make any api calls. It is only getting a access token. You can copy that access token and decode it at http://jwt.ms to see what it contains.

The full module with the functions being called to get a certificate:

using Microsoft.Identity.Client;

using System;
using System.Collections.Generic;
using System.Runtime.ConstrainedExecution;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

namespace MSAL_ClientCred_Cert
{

    /*
     * Reference: https://blogs.aaddevsup.xyz/2019/01/console_keyvault_clientcredentials/ -- for instructions on how to create a certificate
     */
    class Program
    {
        static String accessToken = string.Empty;
        static String client_id = "xxx";
        static String cert_password = client_id;
        static String tenant_id = "xxx.com";
        static String authority = $"https://login.microsoftonline.com/{tenant_id}";
        static List<String> scopes = new List<String>() { $"https://graph.microsoft.com/.default" };
        static String thumbprint = "xxx";

        static void Main(string[] args)
        {

            Console.WriteLine("Hello World!");
            GetToken();
            Console.ReadKey();
        }

        static async void GetToken()
        {
            accessToken = string.Empty;

            IConfidentialClientApplication ica = ConfidentialClientApplicationBuilder.Create(client_id)
                .WithCertificate(GetCert(@"C:\Users\myuser\OneDrive - Microsoft\Documents\Certs\"))
                //.WithCertificate(GetCertByThumbprint( StoreLocation.CurrentUser, thumbprint ) )
                //.WithCertificate( GetCertByName( StoreLocation.CurrentUser, $"{tenant_id}.{client_id}" ) )
                .WithTenantId(tenant_id)
                .WithAuthority(authority)
                .Build();

            AuthenticationResult authResult = null;

            try
            {
                authResult = await ica.AcquireTokenForClient(scopes).ExecuteAsync();
                accessToken = authResult.AccessToken;

                Console.WriteLine($"Access Token: {accessToken}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"MSAL Error: {ex.Message}");
            }


        }

        /// <summary>
        /// Gets a certificate by the .pfx name from the file system
        /// </summary>
        /// <param name="basePath">The folder where the cert is stored -- filename is derived from the settings in this application</param>
        /// <returns></returns>
        static X509Certificate2 GetCert(String basePath)
        {
            String certPath = $"{basePath}{tenant_id}.{client_id}.pfx";
            X509Certificate2 cer = new X509Certificate2(certPath, cert_password, X509KeyStorageFlags.EphemeralKeySet);

            return cer;
        }

        /// <summary>
        /// Gets a certificate based on the name -- please note, there is an issue using this method
        /// if there are multiple certs with the same name.
        /// </summary>
        /// <param name="storeLoc"></param>
        /// <param name="subjectName"></param>
        /// <returns></returns>
        static X509Certificate2 GetCertByName(StoreLocation storeLoc, String subjectName)
        {

            //String certPath = @"C:\Users\myuser\OneDrive - Microsoft\Documents\Certs\MSAL_ClientCred_Cert.cer";
            //X509Certificate2 cer = new X509Certificate2( certPath, client_id, X509KeyStorageFlags.EphemeralKeySet );
            //store.Certificates.Add( cer ); -- use this to import the certificate to the store

            // read certificates already installed
            X509Store store = new X509Store(storeLoc);
            X509Certificate2 cer = null;

            store.Open(OpenFlags.ReadOnly);

            // look for the specific cert by subject name -- returns the first one found, if any.  Also, for self signed, set the valid parameter to 'false'
            X509Certificate2Collection cers = store.Certificates.Find(X509FindType.FindBySubjectName, subjectName, false);
            if (cers.Count > 0)
            {
                cer = cers[0];
            };
            store.Close();

            return cer;

        }

        /// <summary>
        /// Gets a certificate by Thumbprint -- this is the preferred method
        /// </summary>
        /// <param name="storeLoc"></param>
        /// <param name="thumbprint"></param>
        /// <returns></returns>
        static X509Certificate2 GetCertByThumbprint(StoreLocation storeLoc, String thumbprint)
        {
            X509Store store = new X509Store(storeLoc);
            X509Certificate2 cer = null;

            store.Open(OpenFlags.ReadOnly);

            // look for the specific cert by thumbprint -- Also, for self signed, set the valid parameter to 'false'
            X509Certificate2Collection cers = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);
            if (cers.Count > 0)
            {
                cer = cers[0];
            };
            store.Close();

            return cer;
        }
    }
}

Understanding Azure AD token signing certificate (kid)

Introduction

Upon successful authentication, Azure AD issues a signed JWT token (id token or access token). The resource application needs to know the public key of the certificate used sign the token in order to validate the token signature. Depending upon the type (OAuth2 or SAML Application) of the resource application, the steps to obtain the pubic key information are different. An OWIN asp.net application can throw the following error when it’s not able to find the kid to validate the token signature:

IDX10501: Signature validation failed. Unable to match ‘kid’ or IDX10501: Signature validation failed. Unable to match key

The setup:

To demonstrate the concept, I have registered the following 3 Applications in Azure AD:

bhfrontend app – used to sign in and get an access token to one of the following backend app:
bhbackend app – backend oauth2 application which the bhfrontend app requests an access token for
SharepointSAMLTest app – backend SAML application which the bhfrontend app requests an access token for

  1. bhbackend – OAuth2 app (created from Azure Active Directory -> App Registration -> New Registration in the Azure portal).

  1. SharepointSAMLTest – SAML application (created from Azure AD -> Enterprise Application -> New Application -> Non-gallery application -> configure SAML Single sign-on). Note that Azure AD automatically assigns certificate to this application. This certificate is used to sign both a SAML token and an oauth2 token.

  1. bhfrontend – OAuth2 web app (created the same way as bhbackend above). In the API permissions add both bhbackend and SharepointSAMLTest app and grant admin consent. You will also need to create a password secret for this application (to be used later in an authorization code flow to get an access token). This application is used for sign in and get an access token for both the bhbackend app and the SharepointSAMLTest app.

OAuth2 resource application

Use Postman’s Authorization Code flow to obtain an access token for the bhbackend app. For oauth2 applications, use the following keys discovery endpoint to find the public key of the signing certificate: https://login.microsoftonline.com/common/discovery/keys 

I am using https://jwt.ms to decode the access token and kid in the access token should match one of the 3 kids at the end point.

Note: The application demonstrated here is a V1 app. For V2 app, use https://login.microsoftonline.com/common/discovery/v2.0/keys instead of.

SAML Resource Application

Again, use Postman to get an access token for the SharepointSAMLTest App. Azure AD uses the certificate created for this application to sign the token. For this scenario, we have to use the following keys discovery endpoint to get to the public key of that certificate. The App ID below can be found in the Properties section of that Enterprise App.

https://login.microsoftonline.com/<tenant name>/discovery/keys?appid=<SAML App ID>

The above keys discovery endpoint can be obtained from the app-specific metadata endpoint:

https://login.microsoftonline.com/<tenant name>/.well-known/openid-configuration?appid=<SAML App ID>

This is also documented below for both V1 and V2 endpoint:

V2 OpenID Connect protocol
V1 OpenID Connect protocol

If your app has custom signing keys as a result of using the claims-mapping feature, you must append an appid query parameter containing the app ID in order to get a jwks_uri pointing to your app’s signing key information. For example: https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration?appid=6731de76-14a6-49ae-97bc-6eba6914391e contains a jwks_uri of https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys?appid=6731de76-14a6-49ae-97bc-6eba6914391e.

Typically, you would use this metadata document to configure an OpenID Connect library or SDK; the library would use the metadata to do its work. However, if you’re not using a pre-built OpenID Connect library, you can follow the steps in the remainder of this article to do sign-in in a web app by using the Microsoft identity platform endpoint.

In the example above I use the bhfrontend application as OAuth2 application for sign in.  If this application is a SAML Application with Single Sign-On feature enabled, the same Signature Validation error can occur here.  To resolve the error one should use the application-specific metadata endpoint.  See below examples for both the OpenID Connect OWIN middleware of JWT Bearer Authentication middleware usage of the MetadataAddress property.

OpenID Connect middleware:
use OpenIdConnectAuthenticationOptions.MetadataAddress Property

app.UseOpenIdConnectAuthentication(
    new OpenIdConnectAuthenticationOptions
    {
        // Sets the ClientId, authority, RedirectUri as obtained from        web.config
        ClientId = clientId,
        Authority = authority,
        RedirectUri = redirectUri,
        MetadataAddress = "https://login.microsoftonline.com/<tenant name>/.well-known/openid-configuration?appid=<SAML App ID>"
        PostLogoutRedirectUri = redirectUri,
    }

JWT Bearer Authentication middleware:
use JwtBearerOptions.MetadataAddress Property

app.UseJwtBearerAuthentication(new JwtBearerOptions
{
    Audience = "...",
    Authority = "...",
    MetadataAddress = "https://login.microsoftonline.com/<tenant name>/.well-known/openid-configuration?appid=<SAML App ID>",
    TokenValidationParameters = new TokenValidationParameters
    {
       ...
    }
});

Microsoft.Identity.Web Web App Authentication:

services.AddMicrosoftIdentityWebAppAuthentication(Configuration)
     .EnableTokenAcquisitionToCallDownstreamApi()
     .AddInMemoryTokenCaches();
services.Configure<MicrosoftIdentityOptions>(options => { options.MetadataAddress = "https://login.microsoftonline.com/<tenant name>/.well-known/openid-configuration?appid=<SAML App ID>"; });

Microsoft.Identity.Web Web API Authentication:

using Microsoft.AspNetCore.Authentication.JwtBearer;
services.AddMicrosoftIdentityWebApiAuthentication(Configuration);
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme,options => { options.MetadataAddress = "https://login.microsoftonline.com/<tenant name>/.well-known/openid-configuration?appid=<SAML App ID>"; });

Conclusion

I hope this blog post provides clarification on how to get to the public key of the signing certificate to validate the token signature. Please leave us a comment if you find this helpful. If you want to learn more about Azure AD signing keys rollover, you should take a look at this article.