google-auth-library
Version:
Google APIs Authentication Client Library for Node.js
307 lines • 12.7 kB
JavaScript
"use strict";
// Copyright 2026 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.GdchClient = exports.GDCH_SERVICE_ACCOUNT_TYPE = void 0;
const crypto = require("crypto");
const fs = require("fs");
const https = require("https");
const oauth2client_1 = require("./oauth2client");
const DEFAULT_LIFETIME_IN_SECONDS = 3600;
exports.GDCH_SERVICE_ACCOUNT_TYPE = 'gdch_service_account';
class GdchClient extends oauth2client_1.OAuth2Client {
projectId;
privateKeyId;
privateKey;
serviceIdentityName;
tokenServerUri;
caCertPath;
apiAudience;
lifetime;
gdchOptions;
caAgentPromise;
cachedCaCertPath;
lastCaCertReadTime = 0;
CA_CERT_TTL_MS = 5 * 60 * 1000;
constructor(options = {}) {
super(options);
this.gdchOptions = options;
this.projectId = options.projectId || undefined;
this.privateKeyId = options.privateKeyId;
this.privateKey = options.privateKey;
this.serviceIdentityName = options.serviceIdentityName;
this.tokenServerUri = options.tokenServerUri;
this.caCertPath = options.caCertPath;
this.apiAudience = options.apiAudience;
this.lifetime = options.lifetime || DEFAULT_LIFETIME_IN_SECONDS;
// Start with an expired refresh token, which will automatically be
// refreshed before the first API call is made.
this.credentials = { refresh_token: 'gdch-placeholder', expiry_date: 1 };
}
createWithGdchAudience(apiAudience) {
if (!apiAudience) {
throw new Error('Audience cannot be null or empty for GDCH service account credentials.');
}
return new GdchClient({
...this.gdchOptions,
projectId: this.projectId,
privateKeyId: this.privateKeyId,
privateKey: this.privateKey,
serviceIdentityName: this.serviceIdentityName,
tokenServerUri: this.tokenServerUri,
caCertPath: this.caCertPath,
lifetime: this.lifetime,
apiAudience,
});
}
fromJSON(json) {
if (!json) {
throw new Error('Must pass in a JSON object containing the GDCH credentials settings.');
}
if (json.type !== exports.GDCH_SERVICE_ACCOUNT_TYPE) {
throw new Error(`The incoming JSON object does not have the "${exports.GDCH_SERVICE_ACCOUNT_TYPE}" type`);
}
if (json.format_version !== '1') {
throw new Error('Only format version 1 is supported.');
}
if (!json.project) {
throw new Error('The incoming JSON object does not contain a project field');
}
if (!json.private_key_id) {
throw new Error('The incoming JSON object does not contain a private_key_id field');
}
if (!json.private_key) {
throw new Error('The incoming JSON object does not contain a private_key field');
}
if (!json.name) {
throw new Error('The incoming JSON object does not contain a name field');
}
if (!json.token_uri) {
throw new Error('The incoming JSON object does not contain a token_uri field');
}
this.projectId = json.project;
this.privateKeyId = json.private_key_id;
this.privateKey = json.private_key;
this.serviceIdentityName = json.name;
this.tokenServerUri = json.token_uri;
this.caCertPath = json.ca_cert_path;
this.gdchOptions = {
...this.gdchOptions,
projectId: json.project,
privateKeyId: json.private_key_id,
privateKey: json.private_key,
serviceIdentityName: json.name,
tokenServerUri: json.token_uri,
caCertPath: json.ca_cert_path,
};
}
async refreshTokenNoCache() {
if (!this.apiAudience) {
throw new Error('Audience cannot be null or empty for GDCH service account credentials. ' +
'Specify the audience by calling createWithGdchAudience.');
}
if (!this.privateKey) {
throw new Error('Private key is not configured for GDCH credentials.');
}
if (!this.privateKeyId) {
throw new Error('Private key ID is not configured for GDCH credentials.');
}
if (!this.projectId) {
throw new Error('Project is not configured for GDCH credentials.');
}
if (!this.serviceIdentityName) {
throw new Error('Service identity name is not configured for GDCH credentials.');
}
if (!this.tokenServerUri) {
throw new Error('Token server URI is not configured for GDCH credentials.');
}
const assertion = this.createAssertion();
const data = {
audience: this.apiAudience,
grant_type: 'urn:ietf:params:oauth:token-type:token-exchange',
requested_token_type: 'urn:ietf:params:oauth:token-type:access_token',
subject_token: assertion,
subject_token_type: 'urn:k8s:params:oauth:token-type:serviceaccount',
};
const requestOpts = {
url: this.tokenServerUri,
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data,
responseType: 'json',
timeout: 10000,
retry: true,
retryConfig: {
httpMethodsToRetry: ['POST'],
statusCodesToRetry: [[500, 599]],
noResponseRetries: 3,
},
};
if (this.caCertPath) {
requestOpts.agent = await this.getCaAgent();
}
try {
const res = await this.transporter.request(requestOpts);
const tokenResponse = res.data;
if (!tokenResponse.access_token) {
throw new Error('Token response did not contain an access_token.');
}
if (!tokenResponse.expires_in) {
throw new Error('Token response did not contain an expires_in field.');
}
const tokens = {
access_token: tokenResponse.access_token,
token_type: 'STS-Bearer',
expiry_date: Date.now() + tokenResponse.expires_in * 1000,
};
this.emit('tokens', tokens);
return { res, tokens };
}
catch (e) {
if (e && e.config && e.config.data) {
try {
if (typeof e.config.data === 'string') {
const parsedData = JSON.parse(e.config.data);
if (parsedData.subject_token) {
parsedData.subject_token = '***REDACTED***';
e.config.data = JSON.stringify(parsedData);
}
}
else if (typeof e.config.data === 'object' && e.config.data.subject_token) {
e.config.data.subject_token = '***REDACTED***';
}
}
catch { }
}
if (e instanceof Error) {
e.message = `Error getting access token for GDCH service account: ${e.message}, iss: ${this.serviceIdentityName}`;
}
throw e;
}
}
createAssertion() {
const header = {
alg: 'ES256',
typ: 'JWT',
kid: this.privateKeyId,
};
const issSub = `system:serviceaccount:${this.projectId}:${this.serviceIdentityName}`;
const currentTime = Math.floor(Date.now() / 1000);
const payload = {
iss: issSub,
sub: issSub,
iat: currentTime,
exp: currentTime + this.lifetime,
aud: this.tokenServerUri,
};
const encodedHeader = this.base64UrlEncode(JSON.stringify(header));
const encodedPayload = this.base64UrlEncode(JSON.stringify(payload));
const signingInput = `${encodedHeader}.${encodedPayload}`;
const signature = crypto.sign('sha256', Buffer.from(signingInput), {
key: this.privateKey,
dsaEncoding: 'ieee-p1363',
});
const encodedSignature = this.base64UrlEncode(signature);
return `${signingInput}.${encodedSignature}`;
}
async requestAsync(opts, retry = false) {
if (this.caCertPath && !opts.agent) {
const url = (opts.url || '').toString();
if (!url.includes('googleapis.com') && !url.includes('google.com')) {
opts.agent = await this.getCaAgent();
}
}
return super.requestAsync(opts, retry);
}
getCaAgent() {
if (!this.caCertPath) {
this.caAgentPromise = undefined;
this.cachedCaCertPath = undefined;
this.lastCaCertReadTime = 0;
return undefined;
}
const now = Date.now();
const isCacheExpired = now - this.lastCaCertReadTime > this.CA_CERT_TTL_MS;
if (this.caAgentPromise &&
this.caCertPath === this.cachedCaCertPath &&
!isCacheExpired) {
return this.caAgentPromise;
}
this.cachedCaCertPath = this.caCertPath;
this.lastCaCertReadTime = now;
const currentPath = this.caCertPath;
this.caAgentPromise = (async () => {
try {
const ca = await fs.promises.readFile(currentPath);
return new https.Agent({ ca });
}
catch (err) {
if (this.cachedCaCertPath === currentPath) {
this.caAgentPromise = undefined;
this.cachedCaCertPath = undefined;
this.lastCaCertReadTime = 0;
}
if (err instanceof Error) {
err.message = `Error reading certificate file from CA cert path, value '${currentPath}': ${err.message}`;
}
throw err;
}
})();
return this.caAgentPromise;
}
toJSON() {
return {
...this,
privateKey: this.privateKey ? '***REDACTED***' : undefined,
_clientSecret: this._clientSecret ? '***REDACTED***' : undefined,
apiKey: this.apiKey ? '***REDACTED***' : undefined,
gdchOptions: this.gdchOptions
? {
...this.gdchOptions,
privateKey: this.gdchOptions.privateKey ? '***REDACTED***' : undefined,
clientSecret: this.gdchOptions.clientSecret ? '***REDACTED***' : undefined,
client_secret: this.gdchOptions.client_secret ? '***REDACTED***' : undefined,
apiKey: this.gdchOptions.apiKey ? '***REDACTED***' : undefined,
credentials: this.gdchOptions.credentials
? {
...this.gdchOptions.credentials,
access_token: this.gdchOptions.credentials.access_token
? '***REDACTED***'
: undefined,
refresh_token: this.gdchOptions.credentials.refresh_token
? '***REDACTED***'
: undefined,
}
: undefined,
}
: undefined,
credentials: {
...this.credentials,
access_token: this.credentials?.access_token ? '***REDACTED***' : undefined,
refresh_token: this.credentials?.refresh_token ? '***REDACTED***' : undefined,
},
};
}
[Symbol.for('nodejs.util.inspect.custom')]() {
return this.toJSON();
}
base64UrlEncode(str) {
const buffer = typeof str === 'string' ? Buffer.from(str) : str;
return buffer.toString('base64url');
}
}
exports.GdchClient = GdchClient;
//# sourceMappingURL=gdchclient.js.map