DefaultAzureCredential with certificate-based authentication: how to make it work
Tags: node, azure, security
Published: 2023-08-18 07:13:24

## TL;DR;

When using `DefaultAzureCredential`, generate `PEM` certificate and private key files. 
- The `cert.pem` needs to be uploaded to your AAD application registration. 
- Then, create a new `BOTH.pem` file containing **both**, the certificate and the private key. This is the file you need to reference in the `AZURE_CLIENT_CERTIFICATE_PATH`

## The authentication flow

### DefaultAzureCredential class

`DefaultAzureCredential` is a **default credential** capable of automatically handling most Azure SDK authentication scenarios.

The **identity** it uses **depends on the environment**. When an access token is needed, it requests one using these identities in turn, stopping when one provides a token:

- **Environment** - The `DefaultAzureCredential` will read account information specified via [environment variables](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme?view=azure-dotnet#environment-variables) and use it to authenticate.
- **Workload Identity** - If the application is deployed to an Azure host with Workload Identity enabled, the `DefaultAzureCredential` will authenticate with that account.
- **Managed Identity** - If the application is deployed to an Azure host with Managed Identity enabled, the `DefaultAzureCredential` will authenticate with that account.
- etc...

### EnvironmentCredential class

By specifying [environment variables](https://learn.microsoft.com/en-us/python/api/azure-identity/azure.identity.environmentcredential), we instruct the `DefaultAzureCredential` to use `EnvironmentCredential` class. 

The `EnvironmentCredential`, based on the environment variable names, determines how it should authenticate. It effectively acts as a wrapper for the `ClientSecretCredential`, `ClientCertificateCredential` or `UsernamePasswordCredential`.
In all cases, the `AZURE_TENANT_ID` and `AZURE_CLIENT_ID` environment variables are expected to be present to use this credential as they identify your application. The following environment variables will then be tried in order:

- `AZURE_CLIENT_SECRET` - A client secret to be used with `ClientSecretCredential`
- `AZURE_CLIENT_CERTIFICATE_PATH` - The **path to a PEM-formatted certificate file in the deployment environment** to be used with the `ClientCertificateCredential`
- `AZURE_USERNAME` and `AZURE_PASSWORD` - The username and password pair to be used with the `UsernamePasswordCredential`

### ClientCertificateCredential class

Using a certificate to authenticate is recommended as it is generally more secure than using a client secret.

To use this authentication method, you must generate your own PEM-formatted certificate and [register it](https://learn.microsoft.com/en-us/azure/active-directory/develop/certificate-credentials#register-your-certificate-with-microsoft-identity-platform) in the "Certificates & secrets" page for your app registration.

Next, you specify the following environment variables: 

| Variable name | Value|
|-|-|
|`AZURE_CLIENT_ID` |ID of an Azure AD application|
|`AZURE_TENANT_ID` |ID of the application's Azure AD tenant|
|`AZURE_CLIENT_CERTIFICATE_PATH` |path to a **PFX or PEM**-encoded certificate file including **private key** 🤔 |
|`AZURE_CLIENT_CERTIFICATE_PASSWORD` |(optional) the password protecting the certificate file (currently only supported for PFX (PKCS12) certificates)|
|`AZURE_CLIENT_SEND_CERTIFICATE_CHAIN` |(optional) send certificate chain in x5c header to support subject name / issuer based authentication|

### MsalClientCertificate class

The `ClientCertificateCredential` creates an instance of the `MsalClientCertificate` class. According to the [Using certificate credentials with MSAL Node](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/certificate-credentials.md), `MSAL Node` requires the following configuration:

```json
const config = {
    auth: {
        clientId: "YOUR_CLIENT_ID",
        authority: "https://login.microsoftonline.com/YOUR_TENANT_ID",
        clientCertificate: {
            thumbprint: "CERT_THUMBPRINT", // a 40-digit hexadecimal string 
            privateKey: "CERT_PRIVATE_KEY",
        }
    }
};
```

The `MsalClientCertificate` class inherits from `MsalNode` and takes over the job of generating the **certificate thumbprint** and **private key** parameters. 

It first executes [`parseCertificate()`](https://github.com/Azure/azure-sdk-for-js/blob/d8560f0557e614f1fde4616ea164f32a495378a3/sdk/identity/identity/src/msal/nodeFlows/msalClientCertificate.ts#L63C23-L63C39) to generate the **certificate thumbprint** based on the `PEM` file referenced in `AZURE_CLIENT_CERTIFICATE_PATH`, ensuring that the  file contains `-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----`.

Next, if the `certificatePassword` is present, it will decode the `private key` provided in the `AZURE_CLIENT_CERTIFICATE_PATH` file. Otherwise, it returns the **private key** from the `AZURE_CLIENT_CERTIFICATE_PATH` file. 

Do you see what is happening? The `MsalClientCertificate` class expects your local `PEM` file to contain 👉 **BOTH**, the **certificate** file (the same you uploaded to Azure) and the **private key**. 👈

## Wrapping up

### Generate certificate

You may use the following command to generate `PEM` certificates:

```bash
openssl req `
    -x509 `
    -days 365 `
    -newkey rsa:2048 `
    -keyout keyencrypted.pem `
    -out cert.pem `
    -subj '/CN=AuthTestWithPassword/C=CH/ST=Zurich/L=Zurich' `
    -passout pass:HereIsMySuperPass
```

Make sure the key size is at least `2048` bits, as required by [`jsonwebtoken`](https://github.com/auth0/node-jsonwebtoken/wiki/Migration-Notes:-v8-to-v9) version 9.

After creating Service Principal (see [Application and service principal objects in Azure Active Directory](https://learn.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals?tabs=browser)), save the `cert.pem` to "Certificates & secrets"

Next, create a new `PEM` file, containing **BOTH**, the certificate and the private key. The file should have the following format:

#### BOTH.pem

```txt
-----BEGIN CERTIFICATE-----
MIIDfTCCAmWgAwIBAgIUKoqrm/mWZ2EoC/a60vBoAXxoEMEwDQYJKoZIhvcNAQEL
...
tjs1jav+97FKR1lLnyGS90e2LjtTjLzqy1O5k8T1+6sv
-----END CERTIFICATE-----
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIZ6SyJiacCZwCAggA
...
UaTp6QTCR5NvDEb3iPNbQw==
-----END ENCRYPTED PRIVATE KEY-----
```
### Add Environment variables

You may set the environment variable using PowerShell (`$env:Path = 'C:\foo;'`) but I prefer using an `.env` file. It's easier to swap configurations as needed. But remember to only import it when in development mode =)

In the root of your project create `.env.cert` file, and set the required values:

#### .env.cert

```txt
TenantName= "{tenant-name}"
AZURE_CLIENT_ID= "{client-id}"
AZURE_TENANT_ID= "{tenant-id}"
AZURE_CLIENT_CERTIFICATE_PATH= "./temp/BOTH.pem"
AZURE_CLIENT_CERTIFICATE_PASSWORD= "HereIsMySuperPass"

AZURE_CLOUD_INSTANCE= "https://login.microsoftonline.com/"
GRAPH_API_ENDPOINT= "https://graph.microsoft.com/"
```

Or instead, if you are writing **Azure Function**, simply use `local.settings.json` 

#### local.settings.json
```json
{
  "IsEncrypted": false,
  "Values": {
    //...
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "FUNCTIONS_EXTENSION_VERSION": "~4",
    "WEBSITE_NODE_DEFAULT_VERSION": "~18",
    "TenantName": "{tenant-name}",
    "AZURE_CLIENT_ID": "{client-id}",
    "AZURE_TENANT_ID": "{tenant-id}",
    "AZURE_CLIENT_CERTIFICATE_PATH": "./temp/BOTH.pem",
    "AZURE_CLIENT_CERTIFICATE_PASSWORD": "HereIsMySuperPass",
    "AZURE_CLOUD_INSTANCE": "https://login.microsoftonline.com/",
    "GRAPH_API_ENDPOINT": "https://graph.microsoft.com/"
    }
}
```

### Your code

The setup is now ready and you can use the `DefaultAzureCredential` to authenticate. Make sure to import the environment variables from the `.env.cert`

```ts
import dotenv from "dotenv"
import { AccessToken, DefaultAzureCredential } from "@azure/identity";

if (process.env.NODE_ENV !== 'production') {
    dotenv.config({ path: "./.env.cert" })
}

const credential = new DefaultAzureCredential()

const resultGraph: AccessToken = await credential.getToken("https://graph.microsoft.com/.default")
const resultSPO: AccessToken = await credential.getToken(`https://${process.env.TenantName}.sharepoint.com/.default`)

console.log("Auth SPO: ", resultSPO.token.slice(0, 10) + "...")
console.log("Auth Graph: ", resultGraph.token.slice(0, 10) + "...")
```

## Errors

If you do not provide correct local `PEM` file, you may expect the following errors:

### The file at the specified path does not contain a PEM-encoded certificate.

```
Exception: EnvironmentCredential authentication failed. To troubleshoot, visit https://aka.ms/azsdk/js/identity/environmentcredential/troubleshoot. Status code: 400
More details:
The file at the specified path does not contain a PEM-encoded certificate.
```

Reason: The referenced file contains private key only.

### Failed to read private key

```bash
Failed to read private key
Stack: AuthenticationError: EnvironmentCredential authentication failed. To troubleshoot, visit <https://aka.ms/azsdk/js/identity/environmentcredential/troubleshoot>. Status code: 400
```

Reason: The referenced file contains certificate only.