From 1a82bfbfeaac1e971870acf7e4921d3c68439d06 Mon Sep 17 00:00:00 2001 From: Robbie-Microsoft <87724641+Robbie-Microsoft@users.noreply.github.com> Date: Mon, 18 Nov 2024 13:59:51 -0500 Subject: [PATCH] Converted Certificate Sample to TypeScript (#7368) This is the first time we'll have an E2E test running on a TypeScript sample. They all currently run on JS samples. --- package-lock.json | 2 +- samples/msal-node-samples/cliArgs.ts | 41 +++++++++ .../README.md | 37 ++++++--- .../app.ts | 79 ++++++++++++++++++ .../data/cacheTemplate.json | 2 +- .../index.js | 83 ------------------- .../{jest.config.js => jest.config.cjs} | 1 + .../package.json | 9 +- ...tials-with-cert-from-key-vault-aad.spec.ts | 79 +++++++----------- .../test/data/cacheTemplate.json | 2 +- .../tsconfig.json | 6 +- samples/msal-node-samples/tsconfig.json | 1 + 12 files changed, 189 insertions(+), 153 deletions(-) create mode 100644 samples/msal-node-samples/cliArgs.ts create mode 100644 samples/msal-node-samples/client-credentials-with-cert-from-key-vault/app.ts delete mode 100644 samples/msal-node-samples/client-credentials-with-cert-from-key-vault/index.js rename samples/msal-node-samples/client-credentials-with-cert-from-key-vault/{jest.config.js => jest.config.cjs} (85%) diff --git a/package-lock.json b/package-lock.json index 4299f82411..c7005dbd94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64899,7 +64899,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@azure/msal-node": "^2.0.0-beta.0" + "@azure/msal-node": "^2.15.0" }, "devDependencies": { "@types/jest": "^29.5.0", diff --git a/samples/msal-node-samples/cliArgs.ts b/samples/msal-node-samples/cliArgs.ts new file mode 100644 index 0000000000..94c0800778 --- /dev/null +++ b/samples/msal-node-samples/cliArgs.ts @@ -0,0 +1,41 @@ +import yargs from "yargs"; + +interface Arguments { + c: string; + p: number; + r: string | undefined; + s: string; + $0: string; +} + +const argv: Arguments = yargs(process.argv.slice(2)) + .usage("Usage: $0 -p [PORT]") + .options({ + c: { + type: "string", + alias: "cache location", + default: "data/cache.json", + description: + "(Optional) Cache location - default is data/cache.json", + }, + p: { + type: "number", + alias: "port", + default: 3000, + description: "(Optional) Port Number - default is 3000", + }, + r: { + alias: "region", + default: undefined, + description: "(Optional) Region - default is undefined", + }, + s: { + type: "string", + alias: "scenario", + default: "AAD", + description: "(Optional) Scenario name - default is AAD", + }, + }) + .parseSync(); + +export default argv; diff --git a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/README.md b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/README.md index 58dc629860..4faa882fbc 100644 --- a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/README.md +++ b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/README.md @@ -4,7 +4,9 @@ This sample demonstrates how to implement an MSAL Node [confidential client appl 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). +This sample requires an [Azure Key Vault](https://docs.microsoft.com/azure/key-vault/general/basic-concepts). Key Vault and related topics are discussed in [Securing MSAL Node with Azure Key Vault and Azure Managed Identity](../../../lib/msal-node/docs/key-vault-managed-identity.md). + +> :information_source: While you may run this sample locally, you are expected to deploy and run it on **Azure App Service** following the [guide here](../../../lib/msal-node/docs/key-vault-managed-identity.md#using-azure-managed-identity). ## Setup @@ -24,10 +26,9 @@ Locate the folder where `package.json` resides in your terminal. Then type: 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. + - Click on **Upload** certificate and select the certificate file to upload. + - Click **Add**. Once the certificate is uploaded, the _thumbprint_, _start date_, and _expiration_ values are displayed. + - Upload the certificate to your key vault as well 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. @@ -38,16 +39,16 @@ Locate the folder where `package.json` resides in your terminal. Then type: 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 +```typescript const keyVaultSecretClient = await getKeyVaultSecretClient( - "ENTER_KEY_VAULT_URL" + "ENTER_KEY_VAULT_URL" // optional, the "KEY_VAULT_URL" environment variable can be set instead ); [thumbprint, privateKey, x5c] = await getCertificateInfo( keyVaultSecretClient, "ENTER_CERT_NAME" ); -const config = { +config = { auth: { clientId: "ENTER_CLIENT_ID", authority: "https://login.microsoftonline.com/ENTER_TENANT_INFO", @@ -62,14 +63,24 @@ const config = { ## Run the app -In the same folder, type: +Before running the sample (and everytime changes are made to the sample), the TypeScript will need to be compiled. In the same folder, type: ```console - npm start + npx tsc +``` + +This will compile the TypeScript into JavaScript, and put the compiled files in the `/dist` folder. + +The sample can now be run by typing: + +```console + node dist/client-credentials-with-cert-from-key-vault/app.js ``` -After that, you should see the response from Microsoft Entra ID in your terminal. +An npm script, which will run the above npx and node command, has been configured in package.json. To compile and start the sample, type: -## More information +```console + npm start +``` -- [Tutorial: Call the Microsoft Graph API in a Node.js console app](https://docs.microsoft.com/azure/active-directory/develop/tutorial-v2-nodejs-console) +The token returned from Microsoft Entra ID should be immediately displayed in the terminal. diff --git a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/app.ts b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/app.ts new file mode 100644 index 0000000000..e0f77ae5cd --- /dev/null +++ b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/app.ts @@ -0,0 +1,79 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + AuthenticationResult, + ConfidentialClientApplication, + Configuration, + LogLevel, +} from "@azure/msal-node"; +import argv from "../cliArgs"; // command line arguments - see samples/msal-node-samples/cliArgs.ts + +const getClientCredentialsToken = async ( + cca: ConfidentialClientApplication, + clientCredentialRequestScopes: Array, + region?: string +): Promise => { + // With client credentials flows permissions need to be granted in the portal by a tenant administrator. + // The scope is always in the format "/.default" + const clientCredentialRequest = { + scopes: clientCredentialRequestScopes, + azureRegion: region, // (optional) specify the region you will deploy your application to here (e.g. "westus2") + skipCache: true, // (optional) this skips the cache and forces MSAL to get a new token from Azure AD + }; + + try { + const response: AuthenticationResult | null = + await cca.acquireTokenByClientCredential(clientCredentialRequest); + console.log("Response: ", response); + return response; + } catch (error) { + console.log(JSON.stringify(error)); + throw error; + } +}; + +/** + * The code below checks if the script is being executed manually or in automation. + * If the script was executed manually, it will initialize a ConfidentialClientApplication object + * and execute the sample client credentials application. + */ +if (argv.$0 === "dist/client-credentials-with-cert-from-key-vault/app.js") { + (async () => { + const clientConfig: Configuration = { + auth: { + clientId: "", + authority: + "https://login.microsoftonline.com/", + clientCertificate: { + thumbprintSha256: + "", + privateKey: "ENTER_CLIENT_CERTIFICATE_PRIVATE_KEY", + x5c: "ENTER_CLIENT_CERTIFICATE_X5C", + }, + }, + system: { + loggerOptions: { + loggerCallback(loglevel, message, containsPii) { + console.log(message); + }, + piiLoggingEnabled: false, + logLevel: LogLevel.Verbose, + }, + }, + }; + + const confidentialClientApplication: ConfidentialClientApplication = + new ConfidentialClientApplication(clientConfig); + + await getClientCredentialsToken( + confidentialClientApplication, + ["https://graph.microsoft.com/.default"], + argv.r + ); + })(); +} + +export default getClientCredentialsToken; diff --git a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/data/cacheTemplate.json b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/data/cacheTemplate.json index cc8294c3f7..f6798fe097 100644 --- a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/data/cacheTemplate.json +++ b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/data/cacheTemplate.json @@ -4,4 +4,4 @@ "AccessToken": {}, "RefreshToken": {}, "AppMetadata": {} - } \ No newline at end of file +} \ No newline at end of file diff --git a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/index.js b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/index.js deleted file mode 100644 index 9b92700ff8..0000000000 --- a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/index.js +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -var msal = require('@azure/msal-node'); - -/** - * Command line arguments can be used to configure: - * - The port the application runs on - * - The cache file location - * - The authentication scenario/configuration file name - */ -const argv = require("../cliArgs"); - -const cacheLocation = argv.c || "./data/cache.json"; -const cachePlugin = require('../cachePlugin')(cacheLocation); - -/** - * The scenario string is the name of a .json file which contains the MSAL client configuration - * For an example of what a configuration file should look like, check out the customConfig.json file in the - * /config directory. - * - * You can create your own configuration file and replace the path inside the "config" require statement below - * with the path to your custom configuraiton. - */ -const runtimeOptions = argv.ro || null; -const config = require(`./config/AAD.json`); - -function getClientCredentialsToken(cca, clientCredentialRequestScopes, ro) { - // With client credentials flows permissions need to be granted in the portal by a tenant administrator. - // The scope is always in the format "/.default" - const clientCredentialRequest = { - scopes: clientCredentialRequestScopes, - azureRegion: ro ? ro.region : null, // (optional) specify the region you will deploy your application to here (e.g. "westus2") - skipCache: true, // (optional) this skips the cache and forces MSAL to get a new token from Azure AD - }; - - return cca - .acquireTokenByClientCredential(clientCredentialRequest) - .then((response) => { - // Uncomment to see the successful response logged - // console.log("Response: ", response); - }).catch((error) => { - // Uncomment to see the errors logges - // console.log(JSON.stringify(error)); - }); -} - -/** - * The code below checks if the script is being executed manually or in automation. - * If the script was executed manually, it will initialize a ConfidentialClientApplication object - * and execute the sample client credentials application. - */ -if(argv.$0 === "index.js") { - const loggerOptions = { - loggerCallback(loglevel, message, containsPii) { -     console.log(message); - }, -     piiLoggingEnabled: false, - logLevel: msal.LogLevel.Verbose, - } - - // Build MSAL ClientApplication Configuration object - const clientConfig = { - auth: config.authOptions, - cache: { - cachePlugin - }, - // Uncomment or comment the code below to enable or disable the MSAL logger respectively - // system: { -     // loggerOptions, -     // } - }; - - // Create msal application object - const confidentialClientApplication = new msal.ConfidentialClientApplication(clientConfig); - - // Execute sample application with the configured MSAL PublicClientApplication - return getClientCredentialsToken(confidentialClientApplication, runtimeOptions); -} - -module.exports = getClientCredentialsToken; \ No newline at end of file diff --git a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/jest.config.js b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/jest.config.cjs similarity index 85% rename from samples/msal-node-samples/client-credentials-with-cert-from-key-vault/jest.config.js rename to samples/msal-node-samples/client-credentials-with-cert-from-key-vault/jest.config.cjs index 279c63b428..cb66d53e60 100644 --- a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/jest.config.js +++ b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/jest.config.cjs @@ -6,4 +6,5 @@ module.exports = { displayName: "Client Credentials with Certificate from Key Vault", preset: "../../e2eTestUtils/jest-puppeteer-utils/jest-preset-no-setup.js", + extensionsToTreatAsEsm: [".ts", ".tsx"], }; diff --git a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/package.json b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/package.json index d91fe72fcc..4af5b95172 100644 --- a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/package.json +++ b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/package.json @@ -5,15 +5,16 @@ "main": "index.js", "private": true, "scripts": { - "start": "node index.js", "test:e2e": "jest", - "build:package": "cd ../../.. && npm run build:all --workspace=lib/msal-node", - "start:build": "npm run build:package && npm start" + "build": "npx tsc", + "start": "npm run build && node dist/client-credentials-with-cert-from-key-vault/app.js", + "build:package": "cd ../../.. && npm run build:all --workspace=lib/msal-node" }, + "type": "module", "author": "", "license": "MIT", "dependencies": { - "@azure/msal-node": "^2.0.0-beta.0" + "@azure/msal-node": "^2.15.0" }, "devDependencies": { "@types/jest": "^29.5.0", diff --git a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/test/client-credentials-with-cert-from-key-vault-aad.spec.ts b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/test/client-credentials-with-cert-from-key-vault-aad.spec.ts index 07fafb13fc..84962a6a50 100644 --- a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/test/client-credentials-with-cert-from-key-vault-aad.spec.ts +++ b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/test/client-credentials-with-cert-from-key-vault-aad.spec.ts @@ -3,22 +3,21 @@ import { validateCacheLocation, NodeCacheTestUtils, } from "e2e-test-utils"; -import { ConfidentialClientApplication } from "@azure/msal-node"; -import config from "../config/AAD.json"; -import { getKeyVaultSecretClient } from "e2e-test-utils/src/KeyVaultUtils"; -import { getCertificateInfo } from "e2e-test-utils/src/CertificateUtils"; +import { + AuthenticationResult, + ConfidentialClientApplication, + Configuration, +} from "@azure/msal-node"; +import { getKeyVaultSecretClient } from "../../../e2eTestUtils/src/KeyVaultUtils"; +import { getCertificateInfo } from "../../../e2eTestUtils/src/CertificateUtils"; import { ENV_VARIABLES, LAB_CERT_NAME, LAB_KEY_VAULT_URL, -} from "e2e-test-utils/src/Constants"; +} from "../../../e2eTestUtils/src/Constants"; +import getClientCredentialsToken from "../app"; const TEST_CACHE_LOCATION = `${__dirname}/data/aad.cache.json`; - -const getClientCredentialsToken = require("../index"); - -const cachePlugin = require("../../cachePlugin.js")(TEST_CACHE_LOCATION); - const clientCredentialRequestScopes = ["https://graph.microsoft.com/.default"]; describe("Client Credentials AAD Prod Tests", () => { @@ -28,6 +27,7 @@ describe("Client Credentials AAD Prod Tests", () => { let thumbprint: string; let privateKey: string; let x5c: string; + let config: Configuration; beforeAll(async () => { await validateCacheLocation(TEST_CACHE_LOCATION); @@ -39,20 +39,23 @@ describe("Client Credentials AAD Prod Tests", () => { LAB_CERT_NAME ); - config.authOptions.clientId = process.env[ENV_VARIABLES.CLIENT_ID]; - config.authOptions.authority = `https://login.microsoftonline.com/${ - process.env[ENV_VARIABLES.TENANT] - }`; - config.authOptions.clientCertificate = { - thumbprintSha256: thumbprint, - privateKey: privateKey, - x5c: x5c, + config = { + auth: { + clientId: process.env[ENV_VARIABLES.CLIENT_ID] as string, + authority: `https://login.microsoftonline.com/${ + process.env[ENV_VARIABLES.TENANT] + }`, + clientCertificate: { + thumbprintSha256: thumbprint, + privateKey: privateKey, + x5c: x5c, + }, + }, }; }); describe("Acquire Token", () => { let confidentialClientApplication: ConfidentialClientApplication; - let server: any; beforeAll(async () => { await NodeCacheTestUtils.resetCache(TEST_CACHE_LOCATION); @@ -62,39 +65,17 @@ describe("Client Credentials AAD Prod Tests", () => { await NodeCacheTestUtils.resetCache(TEST_CACHE_LOCATION); }); - afterAll(async () => { - if (server) await server.close(); - }); - it("Performs acquire token", async () => { - confidentialClientApplication = new ConfidentialClientApplication({ - auth: config.authOptions, - cache: { cachePlugin }, - }); - server = await getClientCredentialsToken( - confidentialClientApplication, - clientCredentialRequestScopes - ); - const cachedTokens = await NodeCacheTestUtils.getTokens( - TEST_CACHE_LOCATION + confidentialClientApplication = new ConfidentialClientApplication( + config ); - expect(cachedTokens.accessTokens.length).toBe(1); - }); - it("Performs acquire token through regional authorities", async () => { - confidentialClientApplication = new ConfidentialClientApplication({ - auth: config.authOptions, - cache: { cachePlugin }, - }); - server = await getClientCredentialsToken( - confidentialClientApplication, - clientCredentialRequestScopes, - { region: "westus2" } - ); - const cachedTokens = await NodeCacheTestUtils.getTokens( - TEST_CACHE_LOCATION - ); - expect(cachedTokens.accessTokens.length).toBe(1); + const authenticationResult: AuthenticationResult = + await getClientCredentialsToken( + confidentialClientApplication, + clientCredentialRequestScopes + ); + expect(authenticationResult.accessToken).toBeTruthy(); }); }); }); diff --git a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/test/data/cacheTemplate.json b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/test/data/cacheTemplate.json index cc8294c3f7..f6798fe097 100644 --- a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/test/data/cacheTemplate.json +++ b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/test/data/cacheTemplate.json @@ -4,4 +4,4 @@ "AccessToken": {}, "RefreshToken": {}, "AppMetadata": {} - } \ No newline at end of file +} \ No newline at end of file diff --git a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/tsconfig.json b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/tsconfig.json index a8df22f95c..e75f486295 100644 --- a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/tsconfig.json +++ b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/tsconfig.json @@ -1,3 +1,7 @@ { - "extends": "../tsconfig.json" + "extends": "../tsconfig.json", + "files": [ + "app.ts", + "../cliArgs.ts" + ], } \ No newline at end of file diff --git a/samples/msal-node-samples/tsconfig.json b/samples/msal-node-samples/tsconfig.json index 225834c51b..1609257cb5 100644 --- a/samples/msal-node-samples/tsconfig.json +++ b/samples/msal-node-samples/tsconfig.json @@ -22,6 +22,7 @@ "node", "jest" ], + "outDir": "dist", }, "include": [ "**/test/*.spec.ts"