在 MSAL Node 中使用证书凭据

可以使用 MSAL Node(Web 应用、守护程序应用等)生成机密客户端应用程序。 机密客户端必须使用 客户端凭据

先决条件

可以使用 MSAL Node(Web 应用、守护程序应用等)生成机密客户端应用程序。 机密客户端必须使用 客户端凭据 。 客户端凭据可以是:

  • managed identity:这是一种无证书方案,可通过Azure基础结构建立信任。 不需要机密/证书管理。 MSAL 尚未实现此功能,但你可以使用Azure标识 SDK。 请参阅“Azure 资源的托管标识”文档
  • clientSecret:在应用注册期间生成的机密字符串,或者在现有应用程序注册后更新的机密字符串。 不建议在生产环境中使用此功能。
  • clientCertificate:应用注册期间设置的证书。 证书需要具有私钥,因为它用于对 MSAL 生成的 断言 进行签名。 thumbprintSha256是证书的 X.509 SHA-256 指纹,是 privateKey PEM 编码的私钥。
  • clientAssertion:应用开发人员无需让 MSAL 创建 断言,而是控制该断言。 用于向断言添加额外声明或使用 KeyVault 进行签名,而不是使用本地证书。 用于对断言进行签名的证书仍需要在应用注册期间设置。

注意:可能还需要 1p 应用才能发送 x5c。 这是使用者名称/颁发者身份验证方案中使用的 X.509 证书链。

安全地使用机密和证书

不应对机密进行硬编码。 dotenv npm 包可用于将机密或证书存储在 .env 文件(位于项目的根目录中)中,该文件应包含在 .gitignore 中,以防止意外上传机密。

还可以通过 NodeJS 的 fs 模块从文件中读取证书。 但是,不应将它们存储在项目的目录中。 生产应用应从 Azure KeyVault 或其他安全密钥保管库中提取证书。

有关详细信息 ,请参阅证书和机密

请参阅 MSAL 示例: auth-code-with-certs

证书注册

如果没有证书,可以使用 PowerShell 或使用 Azure KeyVault 创建自签名证书。

需要将证书上传到Microsoft Entra ID

  1. 导航到Azure门户并选择Microsoft Entra应用注册。
  2. 在左侧选择“证书和密码”边栏选项。
  3. 单击“ 上传 证书”,然后选择要上传的证书文件(例如 example.crt)。
  4. 单击 添加。 上传证书后,将显示 指纹(SHA-256)开始日期过期 值。

有关详细信息,请参阅:将证书注册到 Microsoft 标识平台

使用证书初始化 MSAL 节点

const msal = require('@azure/msal-node');
require('dotenv').config(); // process.env now has the values defined in a .env file

const config = {
    auth: {
        clientId: "YOUR_CLIENT_ID",
        authority: "https://login.microsoftonline.com/YOUR_TENANT_ID",
        clientCertificate: {
            thumbprintSha256: process.env.thumbprint,
            privateKey: process.env.privateKey,
        }
    }
};

// Create msal application object
const cca = new msal.ConfidentialClientApplication(config);

thumbprintSha256privateKey两者应为字符串。 privateKey 还应为以下格式(PKCS#8):

-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDkpKPrsfpIijS3
z2HCpDsa7dxOsKIrm7F1AtGBjyB0yVDjlh/FA7jT5sd2ypBh3FVsZGJudQsLRKfE
// ...
-----END ENCRYPTED PRIVATE KEY-----

注释

或者,私钥可能以 -----BEGIN PRIVATE KEY----- (未加密的 PKCS#8)或 -----BEGIN RSA PRIVATE KEY-----PKCS#1)开头。 这些格式也是允许的。 以下内容可用于将任何兼容的密钥转换为 PKCS#8 密钥类型:

openssl pkcs8 -topk8 -inform PEM -outform PEM -in example.key -out example.key

如果已使用 通行短语加密私钥(或者私钥已加密),则需要在将私钥传递给 MSAL 节点之前对其进行解密。

重要说明:从不对源代码中的密码进行硬编码。 证书私钥和可选的反加密密码都应从安全位置(例如Azure KeyVault)提取,并使用 Web API 安全地部署。

可以使用 Node 的 加密模块完成此操作。 使用此方法 createPrivateKey() 分析和导出密钥:

const fs = require('fs');
const crypto = require('crypto');

const privateKeySource = fs.readFileSync('<path_to_key>/example.key')

const privateKeyObject = crypto.createPrivateKey({
    key: privateKeySource,
    passphrase: process.env.YOUR_PASSPHRASE,
    format: 'pem'
});

const privateKey = privateKeyObject.export({
    format: 'pem',
    type: 'pkcs8'
});

(可选)将 pfx 转换为 pem

OpenSSL 可用于将 pfx 编码的证书文件转换为 pem

    openssl pkcs12 -in certificate.pfx -out certificate.pem

如果转换需要以编程方式进行,则可能必须依赖于第三方包,因为 Node.js 没有为此提供本机方法。 例如,使用常用的 TLS 实现(如 节点伪造)可以执行以下操作:

const forge = require('node-forge');

/**
 * @param {string} pfx: certificate + private key combination in pfx format
 * @param {string} passphrase: passphrase used to encrypt pfx file
 * @returns {Object}
 */
function convertPFX(pfx, passphrase = null) {

    const asn = forge.asn1.fromDer(forge.util.decode64(pfx));
    const p12 = forge.pkcs12.pkcs12FromAsn1(asn, true, passphrase);

    // Retrieve key data
    const keyData = p12.getBags({ bagType: forge.pki.oids.pkcs8ShroudedKeyBag })[forge.pki.oids.pkcs8ShroudedKeyBag]
        .concat(p12.getBags({ bagType: forge.pki.oids.keyBag })[forge.pki.oids.keyBag]);

    // Retrieve certificate data
    const certBags = p12.getBags({ bagType: forge.pki.oids.certBag })[forge.pki.oids.certBag];
    const certificate = forge.pki.certificateToPem(certBags[0].cert)

    // Convert a Forge private key to an ASN.1 RSAPrivateKey
    const rsaPrivateKey = forge.pki.privateKeyToAsn1(keyData[0].key);

    // Wrap an RSAPrivateKey ASN.1 object in a PKCS#8 ASN.1 PrivateKeyInfo
    const privateKeyInfo = forge.pki.wrapRsaPrivateKey(rsaPrivateKey);

    // Convert a PKCS#8 ASN.1 PrivateKeyInfo to PEM
    const privateKey = forge.pki.privateKeyInfoToPem(privateKeyInfo);

    console.log("Converted certificate: \n", certificate);
    console.log("Converted key: \n", privateKey);

    return {
        certificate: certificate,
        key: privateKey
    };
}

(可选)创建 HTTPS 服务器

OAuth 2.0 协议建议尽可能使用 HTTPS 连接。 大多数云服务(如 Azure 应用服务)默认通过代理提供 HTTPS 连接。 如果出于测试目的,想要设置自己的 HTTPS 服务器,请参阅 Node.js 文档,获取有关创建 HTTPS 服务器的指南。

还需要将自签名证书添加到 OS 的 / 密钥链,以绕过浏览器的安全策略。 之后,仍可能会在浏览器中看到警告(例如 Chrome)。

Warning

可能需要 管理员 权限才能运行上述命令。

常见问题

在某些情况下,尝试使用证书进行身份验证时,可能会收到来自Microsoft Entra ID的错误,例如AADSTS700027: Client assertion contains an invalid signature错误,指示用于初始化 MSAL 节点的证书和/或私钥格式不正确。 一个常见原因是,你提供给 MSAL Node 的证书/私钥字符串中包含意外字符,例如 回车符\r)或 换行符\n):

-----BEGIN CERTIFICATE-----\nMIIDDzCCAfegAwIBAgIJAMkyzQVK88NHMA0GCSqGSIb3DQEBBQUAMIGCMQswCQYDVQQGEwJTRTESMBAGA1UECBMJU3RvY2tob2xtMQ4wDAYDVQQHEwVLaXN0YTEQMA4G0fbkqbKulrchGbNgkankZtEVg4PGjobZq7B+njvcVa7SsWF/WLq5AUbw==\r\n-----END CERTIFICATE-----

或者,证书/密钥文件可能包含 包属性

Bag Attributes
    localKeyID: 28 B5 8E 16 11 88 E9 00 58 D5 76 30 12 B9 59 B8 E4 CE 7C AA
subject=/C=UK/ST=Suffolk/L=Ipswich/O=Example plc/CN=alice
issuer=/C=UK/ST=Suffolk/L=Ipswich/O=Example plc/CN=Certificate Authority/emailAddress=ca@example.com\n
-----BEGIN CERTIFICATE-----
MIIDDzCCAfegAwIBAgIJAMkyzQVK88NHMA0GCSqGSIb3DQEBBQUAMIGCMQswCQYD
VQQGEwJTRTESMBAGA1UECBMJU3RvY2tob2xtMQ4wDAYDVQQHEwVLaXN0YTEQMA4G
0fbkqbKulrchGbNgkankZtEVg4PGjo+Y8MdMjtfSZB29hwYvfMX09jzJ68ZqmpYQ
njvcVtLbEZN5OGCkaslb/f2OxLbsUNgIbws538WnaaufDvKmQe2kUdWmpl9Wn9Bf
bZq7B+njvcVa7SsWF/WLq5AUbw==
-----END CERTIFICATE-----

在这种情况下,您有责任在将该字符串传递给 MSAL Node 配置之前先对其进行清理。 例如:

const msal = require('@azure/msal-node');
const fs = require('fs');

const privateKeySource = fs.readFileSync('<path_to_key>/certs/example.key');
const privateKey = Buffer.from(privateKeySource, 'base64').toString().replace(/\r/g, "").replace(/\n/g, "");

const config = {
    auth: {
        clientId: "YOUR_CLIENT_ID",
        authority: "https://login.microsoftonline.com/YOUR_TENANT_ID",
        clientCertificate: {
            thumbprintSha256: process.env.thumbprint,
            privateKey: privateKey,
        }
    }
};

// Create msal application object
const cca = new msal.ConfidentialClientApplication(config);

另见