wangzhibo
2026-05-09 53418655c061ac26f66058253f39200aced1ab1d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
"use strict";
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
Object.defineProperty(exports, "__esModule", { value: true });
exports.ClientCertificateCredential = void 0;
exports.parseCertificate = parseCertificate;
const msalClient_js_1 = require("../msal/nodeFlows/msalClient.js");
const node_crypto_1 = require("node:crypto");
const tenantIdUtils_js_1 = require("../util/tenantIdUtils.js");
const logging_js_1 = require("../util/logging.js");
const promises_1 = require("node:fs/promises");
const tracing_js_1 = require("../util/tracing.js");
const credentialName = "ClientCertificateCredential";
const logger = (0, logging_js_1.credentialLogger)(credentialName);
/**
 * Enables authentication to Microsoft Entra ID using a PEM-encoded
 * certificate that is assigned to an App Registration. More information
 * on how to configure certificate authentication can be found here:
 *
 * https://learn.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials#register-your-certificate-with-azure-ad
 *
 */
class ClientCertificateCredential {
    tenantId;
    additionallyAllowedTenantIds;
    certificateConfiguration;
    sendCertificateChain;
    msalClient;
    constructor(tenantId, clientId, certificatePathOrConfiguration, options = {}) {
        if (!tenantId || !clientId) {
            throw new Error(`${credentialName}: tenantId and clientId are required parameters.`);
        }
        this.tenantId = tenantId;
        this.additionallyAllowedTenantIds = (0, tenantIdUtils_js_1.resolveAdditionallyAllowedTenantIds)(options?.additionallyAllowedTenants);
        this.sendCertificateChain = options.sendCertificateChain;
        this.certificateConfiguration = {
            ...(typeof certificatePathOrConfiguration === "string"
                ? {
                    certificatePath: certificatePathOrConfiguration,
                }
                : certificatePathOrConfiguration),
        };
        const certificate = this.certificateConfiguration
            .certificate;
        const certificatePath = this.certificateConfiguration
            .certificatePath;
        if (!this.certificateConfiguration || !(certificate || certificatePath)) {
            throw new Error(`${credentialName}: Provide either a PEM certificate in string form, or the path to that certificate in the filesystem. To troubleshoot, visit https://aka.ms/azsdk/js/identity/serviceprincipalauthentication/troubleshoot.`);
        }
        if (certificate && certificatePath) {
            throw new Error(`${credentialName}: To avoid unexpected behaviors, providing both the contents of a PEM certificate and the path to a PEM certificate is forbidden. To troubleshoot, visit https://aka.ms/azsdk/js/identity/serviceprincipalauthentication/troubleshoot.`);
        }
        this.msalClient = (0, msalClient_js_1.createMsalClient)(clientId, tenantId, {
            ...options,
            logger,
            tokenCredentialOptions: options,
        });
    }
    /**
     * Authenticates with Microsoft Entra ID and returns an access token if successful.
     * If authentication fails, a {@link CredentialUnavailableError} will be thrown with the details of the failure.
     *
     * @param scopes - The list of scopes for which the token will have access.
     * @param options - The options used to configure any requests this
     *                TokenCredential implementation might make.
     */
    async getToken(scopes, options = {}) {
        return tracing_js_1.tracingClient.withSpan(`${credentialName}.getToken`, options, async (newOptions) => {
            newOptions.tenantId = (0, tenantIdUtils_js_1.processMultiTenantRequest)(this.tenantId, newOptions, this.additionallyAllowedTenantIds, logger);
            const arrayScopes = Array.isArray(scopes) ? scopes : [scopes];
            const certificate = await this.buildClientCertificate();
            return this.msalClient.getTokenByClientCertificate(arrayScopes, certificate, newOptions);
        });
    }
    async buildClientCertificate() {
        const parts = await parseCertificate(this.certificateConfiguration, this.sendCertificateChain ?? false);
        let privateKey;
        if (this.certificateConfiguration.certificatePassword !== undefined) {
            privateKey = (0, node_crypto_1.createPrivateKey)({
                key: parts.certificateContents,
                passphrase: this.certificateConfiguration.certificatePassword,
                format: "pem",
            })
                .export({
                format: "pem",
                type: "pkcs8",
            })
                .toString();
        }
        else {
            privateKey = parts.certificateContents;
        }
        return {
            thumbprint: parts.thumbprint,
            thumbprintSha256: parts.thumbprintSha256,
            privateKey,
            x5c: parts.x5c,
        };
    }
}
exports.ClientCertificateCredential = ClientCertificateCredential;
/**
 * Parses a certificate into its relevant parts
 *
 * @param certificateConfiguration - The certificate contents or path to the certificate
 * @param sendCertificateChain - true if the entire certificate chain should be sent for SNI, false otherwise
 * @returns The parsed certificate parts and the certificate contents
 */
async function parseCertificate(certificateConfiguration, sendCertificateChain) {
    const certificate = certificateConfiguration.certificate;
    const certificatePath = certificateConfiguration
        .certificatePath;
    const certificateContents = certificate || (await (0, promises_1.readFile)(certificatePath, "utf8"));
    const x5c = sendCertificateChain ? certificateContents : undefined;
    const certificatePattern = /(-+BEGIN CERTIFICATE-+)(\n\r?|\r\n?)([A-Za-z0-9+/\n\r]+=*)(\n\r?|\r\n?)(-+END CERTIFICATE-+)/g;
    const publicKeys = [];
    // Match all possible certificates, in the order they are in the file. These will form the chain that is used for x5c
    let match;
    do {
        match = certificatePattern.exec(certificateContents);
        if (match) {
            publicKeys.push(match[3]);
        }
    } while (match);
    if (publicKeys.length === 0) {
        throw new Error("The file at the specified path does not contain a PEM-encoded certificate.");
    }
    const thumbprint = (0, node_crypto_1.createHash)("sha1") // CodeQL [SM04514] Needed for backward compatibility reason
        .update(Buffer.from(publicKeys[0], "base64"))
        .digest("hex")
        .toUpperCase();
    const thumbprintSha256 = (0, node_crypto_1.createHash)("sha256")
        .update(Buffer.from(publicKeys[0], "base64"))
        .digest("hex")
        .toUpperCase();
    return {
        certificateContents,
        thumbprintSha256,
        thumbprint,
        x5c,
    };
}
//# sourceMappingURL=clientCertificateCredential.js.map