UNPKG

google-auth-library

Version:
307 lines 12.7 kB
"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