-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added E2E Test: Client Credentials with Certificate from Key Vault (#…
…7367) This PR adds a sample with a corresponding e2e test. A follow up PR that converts the sample to TypeScript is located [here](#7368). Additionally, some potential follow up work would be to re-use some of the certificate transforming functionality introduced here and change the way developers pass in certificates. --------- Co-authored-by: Hector Morales <hemoral@microsoft.com>
- Loading branch information
1 parent
098a957
commit dbc0040
Showing
16 changed files
with
492 additions
and
27 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
|
||
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}` | ||
); | ||
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); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
3 changes: 3 additions & 0 deletions
3
samples/msal-node-samples/client-credentials-with-cert-from-key-vault/.beachballrc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"shouldPublish": false | ||
} |
1 change: 1 addition & 0 deletions
1
samples/msal-node-samples/client-credentials-with-cert-from-key-vault/.npmrc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
package-lock=false |
75 changes: 75 additions & 0 deletions
75
samples/msal-node-samples/client-credentials-with-cert-from-key-vault/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.