Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added E2E Test: Client Credentials with Certificate from Key Vault #7367

Open
wants to merge 37 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
772e5b2
Added e2e test
Robbie-Microsoft Sep 11, 2024
eeb3b26
added env variables
Robbie-Microsoft Sep 11, 2024
bf19bfa
Cleaned up code
Robbie-Microsoft Sep 11, 2024
dea84ea
added e2e test to 3p-e2e.yml
Robbie-Microsoft Sep 11, 2024
feb5a8f
downgrade msal-node version
Robbie-Microsoft Sep 11, 2024
971d7ac
update package-lock
Robbie-Microsoft Sep 11, 2024
56872d7
added console.logs to see output on the pipeline
Robbie-Microsoft Sep 11, 2024
530c949
edited console.logs
Robbie-Microsoft Sep 11, 2024
a97b798
added more console.logs
Robbie-Microsoft Sep 11, 2024
b41565b
Merge branch 'dev' into keyvault_cert_sample
Robbie-Microsoft Sep 16, 2024
d5092ba
Merge branch 'dev' into keyvault_cert_sample
Robbie-Microsoft Sep 23, 2024
add9a9e
Merge branch 'dev' into keyvault_cert_sample
Robbie-Microsoft Sep 25, 2024
3678793
Change pipeline branch reference
hectormmg Sep 25, 2024
2eed670
adding AZURE_CLIENT_SEND_CERTIFICATE_CHAIN=true
Robbie-Microsoft Oct 2, 2024
eda7115
removed AZURE_CLIENT_SEND_CERTIFICATE_CHAIN=true, it is now set in th…
Robbie-Microsoft Oct 2, 2024
52c1ea9
bumped @azure/identity to latest
Robbie-Microsoft Oct 2, 2024
76652bf
Merge branch 'dev' into keyvault_cert_sample
Robbie-Microsoft Oct 2, 2024
7036528
cert_path -> certificate_path
Robbie-Microsoft Oct 2, 2024
5f1b62a
changed pipeline source branch, for testing
Robbie-Microsoft Oct 2, 2024
7418b43
removed console.logs
Robbie-Microsoft Oct 2, 2024
5e72945
changed pipeline source branch, for testing
Robbie-Microsoft Oct 3, 2024
3a1d677
changed pipeline source branch, for testing
Robbie-Microsoft Oct 3, 2024
a1bc84f
added comment to gen_env script
Robbie-Microsoft Oct 3, 2024
132244c
updated README
Robbie-Microsoft Oct 3, 2024
436d82f
Merge branch 'dev' into keyvault_cert_sample
Robbie-Microsoft Oct 3, 2024
0524b0a
Merge branch 'dev' into rename_cert_path
Robbie-Microsoft Oct 3, 2024
7d2551a
Merge branch 'rename_cert_path' into keyvault_cert_sample
Robbie-Microsoft Oct 3, 2024
7f8465d
Merge branch 'dev' into keyvault_cert_sample
Robbie-Microsoft Oct 4, 2024
7f300af
undid pipeline ref changes for testing
Robbie-Microsoft Oct 4, 2024
4f6cad6
updated package-lock
Robbie-Microsoft Oct 4, 2024
717d146
Merge branch 'dev' into keyvault_cert_sample
Robbie-Microsoft Oct 8, 2024
14022f8
undid changes to package-lock.json
Robbie-Microsoft Oct 8, 2024
8a37cba
ran npm install with changes in e2e package.json
Robbie-Microsoft Oct 8, 2024
772a9a6
Merge branch 'dev' into keyvault_cert_sample_2
Robbie-Microsoft Oct 9, 2024
6cb554c
Merge branch 'dev' into keyvault_cert_sample_2
Robbie-Microsoft Oct 18, 2024
1255dc0
bumped @azure/identity to 4.5.0
Robbie-Microsoft Oct 18, 2024
52a9741
Merge branch 'dev' into keyvault_cert_sample_2
Robbie-Microsoft Oct 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 51 additions & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion samples/e2eTestUtils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"private": true,
"main": "src/index.ts",
"dependencies": {
"@azure/identity": "^4.3.0",
"@azure/identity": "^4.5.0",
"axios": "^1.7.4",
"dotenv": "^8.2.0",
"find-process": "^1.4.4"
Expand Down
91 changes: 91 additions & 0 deletions samples/e2eTestUtils/src/CertificateUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

const { X509Certificate, createPrivateKey } = require("crypto");
const path = require("path");
const fs = require("fs");
const { execSync } = require("child_process");

import { getKeyVaultSecret } from "./KeyVaultUtils";

// define paths for temporary files
const p12FilePath = path.join(__dirname, "certificate.p12");
const certificateKEY = path.join(__dirname, "certificate.key");
const certificateCER = path.join(__dirname, "certificate.cer");

export const getCertificateInfo = async (
client: any,
secretName: string
): Promise<Array<string>> => {
const PKCS12CertificateBase64: string = await getKeyVaultSecret(
client,
secretName
);

// get the private key and the public certificate, in PKCS 12 format
const pkcs12Certificate = Buffer.from(PKCS12CertificateBase64, "base64");

// write the PKCS#12 certificate to a temporary file
fs.writeFileSync(p12FilePath, pkcs12Certificate);
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved

try {
// get the private key from the pkcs12 file through openssl, via a synchronous child process
execSync(
`openssl pkcs12 -in ${p12FilePath} -nocerts -nodes -passin pass: | sed -ne '/-BEGIN PRIVATE KEY-/,/-END PRIVATE KEY-/p' > ${certificateKEY}`
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved
);
const privateKey: string = fs.readFileSync(certificateKEY, "utf-8");
// this will be used to check if the private key matches the x5c, which ensures the x5c is in the correct order
const privateKeyObject = createPrivateKey(privateKey);

// get the x5c from the pkcs12 file through openssl, via a synchronous child process
execSync(
`openssl pkcs12 -in ${p12FilePath} -nokeys -nodes -passin pass: | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > ${certificateCER}`
);
let x5c: string = fs.readFileSync(certificateCER, "utf-8");

// get a string list of the certificates from the x5c, where the strings will include -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----
const certificates = x5c.split(/(?=-----BEGIN CERTIFICATE-----\n)/g);

const x509FromFirstCertificate = new X509Certificate(certificates[0]);

// check if the private key matches the first certificate in the x5c
let thumbprint: string = "";
if (!x509FromFirstCertificate.checkPrivateKey(privateKeyObject)) {
const x509FromLastCertificate = new X509Certificate(
certificates[certificates.length - 1]
);

// if it doesn't match, the x5c may be reversed (this is common when exporting certificates from azure key vault)
// check if the private key matches the last certificate in the x5c
if (x509FromLastCertificate.checkPrivateKey(privateKeyObject)) {
// if it does, reverse the certs in the x5c
x5c = certificates.reverse().join("");
// format the thumbprint // A:B:C -> ABC
thumbprint = x509FromLastCertificate.fingerprint256.replaceAll(
":",
""
);
} else {
// if it doesn't match, the certificate is malformed
throw "Certificate is malformed";
}
} else {
// format the thumbprint // A:B:C -> ABC
thumbprint = x509FromFirstCertificate.fingerprint256.replaceAll(
":",
""
);
}

return [thumbprint, privateKey, x5c];
} catch (error) {
throw `Error processing PKCS#12 file: ${error}`;
} finally {
// clean up temporary files
fs.unlinkSync(p12FilePath);
fs.unlinkSync(certificateKEY);
fs.unlinkSync(certificateCER);
}
};
2 changes: 2 additions & 0 deletions samples/e2eTestUtils/src/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export const ENV_VARIABLES = {

export const LAB_API_ENDPOINT = "https://msidlab.com/api";
export const LAB_SCOPE = "https://request.msidlab.com/.default";
export const LAB_KEY_VAULT_URL = "https://msidlabs.vault.azure.net";
export const LAB_CERT_NAME = "LabAuth";
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved

export const ParamKeys = {
AZURE_ENVIRONMENT: "azureenvironment",
Expand Down
32 changes: 23 additions & 9 deletions samples/e2eTestUtils/src/KeyVaultUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,40 @@
const { DefaultAzureCredential } = require("@azure/identity");
const { SecretClient } = require("@azure/keyvault-secrets");

export const getKeyVaultSecretClient = (): Promise<void> => {
export const getKeyVaultSecretClient = (
keyVaultUrl?: string
): Promise<void> => {
return new Promise<void>(async (resolve, reject) => {
// DefaultAzureCredential expects the following three environment variables:
// DefaultAzureCredential expects the following four environment variables:
// * AZURE_TENANT_ID: The tenant ID in Azure Active Directory
// * AZURE_CLIENT_ID: The application (client) ID registered in the AAD tenant
// * AZURE_CLIENT_SECRET: The client secret for the registered application
// * AZURE_CLIENT_CERTIFICATE_PATH: The path of the LabAuth cert that was created in 1P's e2e-tests.yml
// * AZURE_CLIENT_SEND_CERTIFICATE_CHAIN: set to "true" - indicates that the LabAuth's x5c header should be sent to the keyvault
const keyVaultCredentials = new DefaultAzureCredential();

try {
const client = await new SecretClient(
process.env["KEY_VAULT_URL"],
keyVaultUrl || process.env["KEY_VAULT_URL"],
keyVaultCredentials
);
resolve(client);
return resolve(client);
} catch (error) {
reject(error);
return reject(error);
}
});
};

export const getKeyVaultSecret = async (
client: any,
secretName: string
): Promise<string> => {
try {
return (await client.getSecret(secretName)).value;
} catch (error) {
throw error;
}
};

export const getCredentials = (client: any): Promise<Array<string>> => {
const usernameSecret: string = "username";
const passwordSecret: string = "password";
Expand All @@ -35,16 +49,16 @@ export const getCredentials = (client: any): Promise<Array<string>> => {
try {
username = await client.getSecret(usernameSecret);
} catch (error) {
reject(error);
return reject(error);
}

let password: any;
try {
password = await client.getSecret(passwordSecret);
} catch (error) {
reject(error);
return reject(error);
}

resolve([username.value, password.value]);
return resolve([username.value, password.value]);
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"shouldPublish": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# MSAL Node Standalone Sample: Client Credentials Grant with Certificate

This sample demonstrates how to implement an MSAL Node [confidential client application](../../../lib/msal-node/docs/initialize-confidential-client-application.md) to acquire an access token with application permissions using the [OAuth 2.0 Client Credentials Grant](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow), via a certificate retrieved from a key vault.

The **Client Credentials** flow is most commonly used for a daemon or a command-line app that calls web APIs and does not have any user interaction.

MSAL Node also supports specifying a **regional authority** for acquiring tokens when using the client credentials flow. For more information on this, please refer to: [Regional Authorities](../../../lib/msal-node/docs/regional-authorities.md).

## Setup

Locate the folder where `package.json` resides in your terminal. Then type:

```console
npm install
```

## Register

1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com) and select the **Microsoft Entra ID** service.
1. Select the **App Registrations** blade on the left, then select **New registration**.
1. In the **Register an application page** that appears, enter your application's registration information:
- In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `msal-node-console`.
- Under **Supported account types**, select **Accounts in this organizational directory only**.
1. Select **Register** to create the application.
1. In the app's registration screen, find and note the **Application (client) ID** and **Directory (Tenant) ID**. You use these values in your app's configuration file(s) later.
1. In the app's registration screen, select the **Certificates & secrets** blade in the left.
- In the **Client secrets** section, select **New client secret**.
- Type a key description (for instance `app secret`),
- Select one of the available key durations (6 months, 12 months or Custom) as per your security posture.
- The generated key value will be displayed when you select the **Add** button. Copy and save the generated value for use in later steps.
1. In the app's registration screen, select the API permissions blade in the left to open the page where we add access to the APIs that your application needs.
- Select the **Add a permission** button and then,
- Ensure that the **Microsoft APIs** tab is selected.
- In the **Commonly used Microsoft APIs** section, select **Microsoft Graph**
- In the **Application permissions** section, select the **User.Read.All** in the list. Use the search box if necessary.
- Select the **Add permissions** button at the bottom.
- Finally, grant **admin consent** for this scope.

Before running the sample, you will need to replace the values in retrieve-cert-from-key-vault code as well as the configuration object:

```javascript
const keyVaultSecretClient = await getKeyVaultSecretClient(
"ENTER_KEY_VAULT_URL"
);
[thumbprint, privateKey, x5c] = await getCertificateInfo(
keyVaultSecretClient,
"ENTER_CERT_NAME"
);

const config = {
auth: {
clientId: "ENTER_CLIENT_ID",
authority: "https://login.microsoftonline.com/ENTER_TENANT_INFO",
clientCertificate: {
thumbprintSha256: thumbprint,
privateKey: privateKey,
x5c: x5c,
},
},
};
```

## Run the app

In the same folder, type:

```console
npm start
```

After that, you should see the response from Microsoft Entra ID in your terminal.

## More information

- [Tutorial: Call the Microsoft Graph API in a Node.js console app](https://docs.microsoft.com/azure/active-directory/develop/tutorial-v2-nodejs-console)
Loading
Loading