google-auth-library
Version:
Google APIs Authentication Client Library for Node.js
223 lines • 10.6 kB
JavaScript
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
Object.defineProperty(exports, "__esModule", { value: true });
exports.CertificateSubjectTokenSupplier = exports.InvalidConfigurationError = exports.CertificateSourceUnavailableError = exports.CERTIFICATE_CONFIGURATION_ENV_VARIABLE = void 0;
const util_1 = require("../util");
const fs = require("fs");
const crypto_1 = require("crypto");
const https = require("https");
exports.CERTIFICATE_CONFIGURATION_ENV_VARIABLE = 'GOOGLE_API_CERTIFICATE_CONFIG';
/**
* Thrown when the certificate source cannot be located or accessed.
*/
class CertificateSourceUnavailableError extends Error {
constructor(message) {
super(message);
this.name = 'CertificateSourceUnavailableError';
}
}
exports.CertificateSourceUnavailableError = CertificateSourceUnavailableError;
/**
* Thrown for invalid configuration that is not related to file availability.
*/
class InvalidConfigurationError extends Error {
constructor(message) {
super(message);
this.name = 'InvalidConfigurationError';
}
}
exports.InvalidConfigurationError = InvalidConfigurationError;
/**
* A subject token supplier that uses a client certificate for authentication.
* It provides the certificate chain as the subject token for identity federation.
*/
class CertificateSubjectTokenSupplier {
certificateConfigPath;
trustChainPath;
cert;
key;
/**
* Initializes a new instance of the CertificateSubjectTokenSupplier.
* @param opts The configuration options for the supplier.
*/
constructor(opts) {
if (!opts.useDefaultCertificateConfig && !opts.certificateConfigLocation) {
throw new InvalidConfigurationError('Either `useDefaultCertificateConfig` must be true or a `certificateConfigLocation` must be provided.');
}
if (opts.useDefaultCertificateConfig && opts.certificateConfigLocation) {
throw new InvalidConfigurationError('Both `useDefaultCertificateConfig` and `certificateConfigLocation` cannot be provided.');
}
this.trustChainPath = opts.trustChainPath;
this.certificateConfigPath = opts.certificateConfigLocation ?? '';
}
/**
* Creates an HTTPS agent configured with the client certificate and private key for mTLS.
* @returns An mTLS-configured https.Agent.
*/
async createMtlsHttpsAgent() {
if (!this.key || !this.cert) {
throw new InvalidConfigurationError('Cannot create mTLS Agent with missing certificate or key');
}
return new https.Agent({ key: this.key, cert: this.cert });
}
/**
* Constructs the subject token, which is the base64-encoded certificate chain.
* @returns A promise that resolves with the subject token.
*/
async getSubjectToken() {
// The "subject token" in this context is the processed certificate chain.
this.certificateConfigPath = await this.#resolveCertificateConfigFilePath();
const { certPath, keyPath } = await this.#getCertAndKeyPaths();
({ cert: this.cert, key: this.key } = await this.#getKeyAndCert(certPath, keyPath));
return await this.#processChainFromPaths(this.cert);
}
/**
* Resolves the absolute path to the certificate configuration file
* by checking the "certificate_config_location" provided in the ADC file,
* or the "GOOGLE_API_CERTIFICATE_CONFIG" environment variable
* or in the default gcloud path.
* @param overridePath An optional path to check first.
* @returns The resolved file path.
*/
async #resolveCertificateConfigFilePath() {
// 1. Check for the override path from constructor options.
const overridePath = this.certificateConfigPath;
if (overridePath) {
if (await (0, util_1.isValidFile)(overridePath)) {
return overridePath;
}
throw new CertificateSourceUnavailableError(`Provided certificate config path is invalid: ${overridePath}`);
}
// 2. Check the standard environment variable.
const envPath = process.env[exports.CERTIFICATE_CONFIGURATION_ENV_VARIABLE];
if (envPath) {
if (await (0, util_1.isValidFile)(envPath)) {
return envPath;
}
throw new CertificateSourceUnavailableError(`Path from environment variable "${exports.CERTIFICATE_CONFIGURATION_ENV_VARIABLE}" is invalid: ${envPath}`);
}
// 3. Check the well-known gcloud config location.
const wellKnownPath = (0, util_1.getWellKnownCertificateConfigFileLocation)();
if (await (0, util_1.isValidFile)(wellKnownPath)) {
return wellKnownPath;
}
// 4. If none are found, throw an error.
throw new CertificateSourceUnavailableError('Could not find certificate configuration file. Searched override path, ' +
`the "${exports.CERTIFICATE_CONFIGURATION_ENV_VARIABLE}" env var, and the gcloud path (${wellKnownPath}).`);
}
/**
* Reads and parses the certificate config JSON file to extract the certificate and key paths.
* @returns An object containing the certificate and key paths.
*/
async #getCertAndKeyPaths() {
const configPath = this.certificateConfigPath;
let fileContents;
try {
fileContents = await fs.promises.readFile(configPath, 'utf8');
}
catch (err) {
throw new CertificateSourceUnavailableError(`Failed to read certificate config file at: ${configPath}`);
}
try {
const config = JSON.parse(fileContents);
const certPath = config?.cert_configs?.workload?.cert_path;
const keyPath = config?.cert_configs?.workload?.key_path;
if (!certPath || !keyPath) {
throw new InvalidConfigurationError(`Certificate config file (${configPath}) is missing required "cert_path" or "key_path" in the workload config.`);
}
return { certPath, keyPath };
}
catch (e) {
if (e instanceof InvalidConfigurationError)
throw e;
throw new InvalidConfigurationError(`Failed to parse certificate config from ${configPath}: ${e.message}`);
}
}
/**
* Reads and parses the cert and key files get their content and check valid format.
* @returns An object containing the cert content and key content in buffer format.
*/
async #getKeyAndCert(certPath, keyPath) {
let cert, key;
try {
cert = await fs.promises.readFile(certPath);
new crypto_1.X509Certificate(cert);
}
catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new CertificateSourceUnavailableError(`Failed to read certificate file at ${certPath}: ${message}`);
}
try {
key = await fs.promises.readFile(keyPath);
(0, crypto_1.createPrivateKey)(key);
}
catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new CertificateSourceUnavailableError(`Failed to read private key file at ${keyPath}: ${message}`);
}
return { cert, key };
}
/**
* Reads the leaf certificate and trust chain, combines them,
* and returns a JSON array of base64-encoded certificates.
* @returns A stringified JSON array of the certificate chain.
*/
async #processChainFromPaths(leafCertBuffer) {
const leafCert = new crypto_1.X509Certificate(leafCertBuffer);
// If no trust chain is provided, just use the successfully parsed leaf certificate.
if (!this.trustChainPath) {
return JSON.stringify([leafCert.raw.toString('base64')]);
}
// Handle the trust chain logic.
try {
const chainPems = await fs.promises.readFile(this.trustChainPath, 'utf8');
const pemBlocks = chainPems.match(/-----BEGIN CERTIFICATE-----[^-]+-----END CERTIFICATE-----/g) ?? [];
const chainCerts = pemBlocks.map((pem, index) => {
try {
return new crypto_1.X509Certificate(pem);
}
catch (err) {
const message = err instanceof Error ? err.message : String(err);
// Throw a more precise error if a single certificate in the chain is invalid.
throw new InvalidConfigurationError(`Failed to parse certificate at index ${index} in trust chain file ${this.trustChainPath}: ${message}`);
}
});
const leafIndex = chainCerts.findIndex(chainCert => leafCert.raw.equals(chainCert.raw));
let finalChain;
if (leafIndex === -1) {
// Leaf not found, so prepend it to the chain.
finalChain = [leafCert, ...chainCerts];
}
else if (leafIndex === 0) {
// Leaf is already the first element, so the chain is correctly ordered.
finalChain = chainCerts;
}
else {
// Leaf is in the chain but not at the top, which is invalid.
throw new InvalidConfigurationError(`Leaf certificate exists in the trust chain but is not the first entry (found at index ${leafIndex}).`);
}
return JSON.stringify(finalChain.map(cert => cert.raw.toString('base64')));
}
catch (err) {
// Re-throw our specific configuration errors.
if (err instanceof InvalidConfigurationError)
throw err;
const message = err instanceof Error ? err.message : String(err);
throw new CertificateSourceUnavailableError(`Failed to process certificate chain from ${this.trustChainPath}: ${message}`);
}
}
}
exports.CertificateSubjectTokenSupplier = CertificateSubjectTokenSupplier;
//# sourceMappingURL=certificatesubjecttokensupplier.js.map
;