Edit File: googleauth.js
"use strict"; // Copyright 2019 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.GoogleAuth = exports.CLOUD_SDK_CLIENT_ID = void 0; const child_process_1 = require("child_process"); const fs = require("fs"); const gcpMetadata = require("gcp-metadata"); const os = require("os"); const path = require("path"); const crypto_1 = require("../crypto/crypto"); const transporters_1 = require("../transporters"); const computeclient_1 = require("./computeclient"); const idtokenclient_1 = require("./idtokenclient"); const envDetect_1 = require("./envDetect"); const jwtclient_1 = require("./jwtclient"); const refreshclient_1 = require("./refreshclient"); const externalclient_1 = require("./externalclient"); const baseexternalclient_1 = require("./baseexternalclient"); exports.CLOUD_SDK_CLIENT_ID = '764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com'; class GoogleAuth { constructor(opts) { /** * Caches a value indicating whether the auth layer is running on Google * Compute Engine. * @private */ this.checkIsGCE = undefined; // To save the contents of the JSON credential file this.jsonContent = null; this.cachedCredential = null; opts = opts || {}; this._cachedProjectId = opts.projectId || null; this.cachedCredential = opts.authClient || null; this.keyFilename = opts.keyFilename || opts.keyFile; this.scopes = opts.scopes; this.jsonContent = opts.credentials || null; this.clientOptions = opts.clientOptions; } // Note: this properly is only public to satisify unit tests. // https://github.com/Microsoft/TypeScript/issues/5228 get isGCE() { return this.checkIsGCE; } // GAPIC client libraries should always use self-signed JWTs. The following // variables are set on the JWT client in order to indicate the type of library, // and sign the JWT with the correct audience and scopes (if not supplied). setGapicJWTValues(client) { client.defaultServicePath = this.defaultServicePath; client.useJWTAccessWithScope = this.useJWTAccessWithScope; client.defaultScopes = this.defaultScopes; } getProjectId(callback) { if (callback) { this.getProjectIdAsync().then(r => callback(null, r), callback); } else { return this.getProjectIdAsync(); } } getProjectIdAsync() { if (this._cachedProjectId) { return Promise.resolve(this._cachedProjectId); } // In implicit case, supports three environments. In order of precedence, // the implicit environments are: // - GCLOUD_PROJECT or GOOGLE_CLOUD_PROJECT environment variable // - GOOGLE_APPLICATION_CREDENTIALS JSON file // - Cloud SDK: `gcloud config config-helper --format json` // - GCE project ID from metadata server) if (!this._getDefaultProjectIdPromise) { // TODO: refactor the below code so that it doesn't mix and match // promises and async/await. this._getDefaultProjectIdPromise = new Promise( // eslint-disable-next-line no-async-promise-executor async (resolve, reject) => { try { const projectId = this.getProductionProjectId() || (await this.getFileProjectId()) || (await this.getDefaultServiceProjectId()) || (await this.getGCEProjectId()) || (await this.getExternalAccountClientProjectId()); this._cachedProjectId = projectId; if (!projectId) { throw new Error('Unable to detect a Project Id in the current environment. \n' + 'To learn more about authentication and Google APIs, visit: \n' + 'https://cloud.google.com/docs/authentication/getting-started'); } resolve(projectId); } catch (e) { reject(e); } }); } return this._getDefaultProjectIdPromise; } /** * @returns Any scopes (user-specified or default scopes specified by the * client library) that need to be set on the current Auth client. */ getAnyScopes() { return this.scopes || this.defaultScopes; } getApplicationDefault(optionsOrCallback = {}, callback) { let options; if (typeof optionsOrCallback === 'function') { callback = optionsOrCallback; } else { options = optionsOrCallback; } if (callback) { this.getApplicationDefaultAsync(options).then(r => callback(null, r.credential, r.projectId), callback); } else { return this.getApplicationDefaultAsync(options); } } async getApplicationDefaultAsync(options = {}) { // If we've already got a cached credential, just return it. if (this.cachedCredential) { return { credential: this.cachedCredential, projectId: await this.getProjectIdAsync(), }; } let credential; let projectId; // Check for the existence of a local environment variable pointing to the // location of the credential file. This is typically used in local // developer scenarios. credential = await this._tryGetApplicationCredentialsFromEnvironmentVariable(options); if (credential) { if (credential instanceof jwtclient_1.JWT) { credential.scopes = this.scopes; } else if (credential instanceof baseexternalclient_1.BaseExternalAccountClient) { credential.scopes = this.getAnyScopes(); } this.cachedCredential = credential; projectId = await this.getProjectId(); return { credential, projectId }; } // Look in the well-known credential file location. credential = await this._tryGetApplicationCredentialsFromWellKnownFile(options); if (credential) { if (credential instanceof jwtclient_1.JWT) { credential.scopes = this.scopes; } else if (credential instanceof baseexternalclient_1.BaseExternalAccountClient) { credential.scopes = this.getAnyScopes(); } this.cachedCredential = credential; projectId = await this.getProjectId(); return { credential, projectId }; } // Determine if we're running on GCE. let isGCE; try { isGCE = await this._checkIsGCE(); } catch (e) { if (e instanceof Error) { e.message = `Unexpected error determining execution environment: ${e.message}`; } throw e; } if (!isGCE) { // We failed to find the default credentials. Bail out with an error. throw new Error('Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.'); } // For GCE, just return a default ComputeClient. It will take care of // the rest. options.scopes = this.getAnyScopes(); this.cachedCredential = new computeclient_1.Compute(options); projectId = await this.getProjectId(); return { projectId, credential: this.cachedCredential }; } /** * Determines whether the auth layer is running on Google Compute Engine. * @returns A promise that resolves with the boolean. * @api private */ async _checkIsGCE() { if (this.checkIsGCE === undefined) { this.checkIsGCE = await gcpMetadata.isAvailable(); } return this.checkIsGCE; } /** * Attempts to load default credentials from the environment variable path.. * @returns Promise that resolves with the OAuth2Client or null. * @api private */ async _tryGetApplicationCredentialsFromEnvironmentVariable(options) { const credentialsPath = process.env['GOOGLE_APPLICATION_CREDENTIALS'] || process.env['google_application_credentials']; if (!credentialsPath || credentialsPath.length === 0) { return null; } try { return this._getApplicationCredentialsFromFilePath(credentialsPath, options); } catch (e) { if (e instanceof Error) { e.message = `Unable to read the credential file specified by the GOOGLE_APPLICATION_CREDENTIALS environment variable: ${e.message}`; } throw e; } } /** * Attempts to load default credentials from a well-known file location * @return Promise that resolves with the OAuth2Client or null. * @api private */ async _tryGetApplicationCredentialsFromWellKnownFile(options) { // First, figure out the location of the file, depending upon the OS type. let location = null; if (this._isWindows()) { // Windows location = process.env['APPDATA']; } else { // Linux or Mac const home = process.env['HOME']; if (home) { location = path.join(home, '.config'); } } // If we found the root path, expand it. if (location) { location = path.join(location, 'gcloud', 'application_default_credentials.json'); if (!fs.existsSync(location)) { location = null; } } // The file does not exist. if (!location) { return null; } // The file seems to exist. Try to use it. const client = await this._getApplicationCredentialsFromFilePath(location, options); return client; } /** * Attempts to load default credentials from a file at the given path.. * @param filePath The path to the file to read. * @returns Promise that resolves with the OAuth2Client * @api private */ async _getApplicationCredentialsFromFilePath(filePath, options = {}) { // Make sure the path looks like a string. if (!filePath || filePath.length === 0) { throw new Error('The file path is invalid.'); } // Make sure there is a file at the path. lstatSync will throw if there is // nothing there. try { // Resolve path to actual file in case of symlink. Expect a thrown error // if not resolvable. filePath = fs.realpathSync(filePath); if (!fs.lstatSync(filePath).isFile()) { throw new Error(); } } catch (err) { if (err instanceof Error) { err.message = `The file at ${filePath} does not exist, or it is not a file. ${err.message}`; } throw err; } // Now open a read stream on the file, and parse it. const readStream = fs.createReadStream(filePath); return this.fromStream(readStream, options); } /** * Create a credentials instance using the given input options. * @param json The input object. * @param options The JWT or UserRefresh options for the client * @returns JWT or UserRefresh Client with data */ fromJSON(json, options) { let client; if (!json) { throw new Error('Must pass in a JSON object containing the Google auth settings.'); } options = options || {}; if (json.type === 'authorized_user') { client = new refreshclient_1.UserRefreshClient(options); client.fromJSON(json); } else if (json.type === baseexternalclient_1.EXTERNAL_ACCOUNT_TYPE) { client = externalclient_1.ExternalAccountClient.fromJSON(json, options); client.scopes = this.getAnyScopes(); } else { options.scopes = this.scopes; client = new jwtclient_1.JWT(options); this.setGapicJWTValues(client); client.fromJSON(json); } return client; } /** * Return a JWT or UserRefreshClient from JavaScript object, caching both the * object used to instantiate and the client. * @param json The input object. * @param options The JWT or UserRefresh options for the client * @returns JWT or UserRefresh Client with data */ _cacheClientFromJSON(json, options) { let client; // create either a UserRefreshClient or JWT client. options = options || {}; if (json.type === 'authorized_user') { client = new refreshclient_1.UserRefreshClient(options); client.fromJSON(json); } else if (json.type === baseexternalclient_1.EXTERNAL_ACCOUNT_TYPE) { client = externalclient_1.ExternalAccountClient.fromJSON(json, options); client.scopes = this.getAnyScopes(); } else { options.scopes = this.scopes; client = new jwtclient_1.JWT(options); this.setGapicJWTValues(client); client.fromJSON(json); } // cache both raw data used to instantiate client and client itself. this.jsonContent = json; this.cachedCredential = client; return client; } fromStream(inputStream, optionsOrCallback = {}, callback) { let options = {}; if (typeof optionsOrCallback === 'function') { callback = optionsOrCallback; } else { options = optionsOrCallback; } if (callback) { this.fromStreamAsync(inputStream, options).then(r => callback(null, r), callback); } else { return this.fromStreamAsync(inputStream, options); } } fromStreamAsync(inputStream, options) { return new Promise((resolve, reject) => { if (!inputStream) { throw new Error('Must pass in a stream containing the Google auth settings.'); } let s = ''; inputStream .setEncoding('utf8') .on('error', reject) .on('data', chunk => (s += chunk)) .on('end', () => { try { try { const data = JSON.parse(s); const r = this._cacheClientFromJSON(data, options); return resolve(r); } catch (err) { // If we failed parsing this.keyFileName, assume that it // is a PEM or p12 certificate: if (!this.keyFilename) throw err; const client = new jwtclient_1.JWT({ ...this.clientOptions, keyFile: this.keyFilename, }); this.cachedCredential = client; this.setGapicJWTValues(client); return resolve(client); } } catch (err) { return reject(err); } }); }); } /** * Create a credentials instance using the given API key string. * @param apiKey The API key string * @param options An optional options object. * @returns A JWT loaded from the key */ fromAPIKey(apiKey, options) { options = options || {}; const client = new jwtclient_1.JWT(options); client.fromAPIKey(apiKey); return client; } /** * Determines whether the current operating system is Windows. * @api private */ _isWindows() { const sys = os.platform(); if (sys && sys.length >= 3) { if (sys.substring(0, 3).toLowerCase() === 'win') { return true; } } return false; } /** * Run the Google Cloud SDK command that prints the default project ID */ async getDefaultServiceProjectId() { return new Promise(resolve => { child_process_1.exec('gcloud config config-helper --format json', (err, stdout) => { if (!err && stdout) { try { const projectId = JSON.parse(stdout).configuration.properties.core.project; resolve(projectId); return; } catch (e) { // ignore errors } } resolve(null); }); }); } /** * Loads the project id from environment variables. * @api private */ getProductionProjectId() { return (process.env['GCLOUD_PROJECT'] || process.env['GOOGLE_CLOUD_PROJECT'] || process.env['gcloud_project'] || process.env['google_cloud_project']); } /** * Loads the project id from the GOOGLE_APPLICATION_CREDENTIALS json file. * @api private */ async getFileProjectId() { if (this.cachedCredential) { // Try to read the project ID from the cached credentials file return this.cachedCredential.projectId; } // Ensure the projectId is loaded from the keyFile if available. if (this.keyFilename) { const creds = await this.getClient(); if (creds && creds.projectId) { return creds.projectId; } } // Try to load a credentials file and read its project ID const r = await this._tryGetApplicationCredentialsFromEnvironmentVariable(); if (r) { return r.projectId; } else { return null; } } /** * Gets the project ID from external account client if available. */ async getExternalAccountClientProjectId() { if (!this.jsonContent || this.jsonContent.type !== baseexternalclient_1.EXTERNAL_ACCOUNT_TYPE) { return null; } const creds = await this.getClient(); // Do not suppress the underlying error, as the error could contain helpful // information for debugging and fixing. This is especially true for // external account creds as in order to get the project ID, the following // operations have to succeed: // 1. Valid credentials file should be supplied. // 2. Ability to retrieve access tokens from STS token exchange API. // 3. Ability to exchange for service account impersonated credentials (if // enabled). // 4. Ability to get project info using the access token from step 2 or 3. // Without surfacing the error, it is harder for developers to determine // which step went wrong. return await creds.getProjectId(); } /** * Gets the Compute Engine project ID if it can be inferred. */ async getGCEProjectId() { try { const r = await gcpMetadata.project('project-id'); return r; } catch (e) { // Ignore any errors return null; } } getCredentials(callback) { if (callback) { this.getCredentialsAsync().then(r => callback(null, r), callback); } else { return this.getCredentialsAsync(); } } async getCredentialsAsync() { await this.getClient(); if (this.jsonContent) { const credential = { client_email: this.jsonContent.client_email, private_key: this.jsonContent.private_key, }; return credential; } const isGCE = await this._checkIsGCE(); if (!isGCE) { throw new Error('Unknown error.'); } // For GCE, return the service account details from the metadata server // NOTE: The trailing '/' at the end of service-accounts/ is very important! // The GCF metadata server doesn't respect querystring params if this / is // not included. const data = await gcpMetadata.instance({ property: 'service-accounts/', params: { recursive: 'true' }, }); if (!data || !data.default || !data.default.email) { throw new Error('Failure from metadata server.'); } return { client_email: data.default.email }; } /** * Automatically obtain a client based on the provided configuration. If no * options were passed, use Application Default Credentials. */ async getClient(options) { if (options) { throw new Error('Passing options to getClient is forbidden in v5.0.0. Use new GoogleAuth(opts) instead.'); } if (!this.cachedCredential) { if (this.jsonContent) { this._cacheClientFromJSON(this.jsonContent, this.clientOptions); } else if (this.keyFilename) { const filePath = path.resolve(this.keyFilename); const stream = fs.createReadStream(filePath); await this.fromStreamAsync(stream, this.clientOptions); } else { await this.getApplicationDefaultAsync(this.clientOptions); } } return this.cachedCredential; } /** * Creates a client which will fetch an ID token for authorization. * @param targetAudience the audience for the fetched ID token. * @returns IdTokenClient for making HTTP calls authenticated with ID tokens. */ async getIdTokenClient(targetAudience) { const client = await this.getClient(); if (!('fetchIdToken' in client)) { throw new Error('Cannot fetch ID token in this environment, use GCE or set the GOOGLE_APPLICATION_CREDENTIALS environment variable to a service account credentials JSON file.'); } return new idtokenclient_1.IdTokenClient({ targetAudience, idTokenProvider: client }); } /** * Automatically obtain application default credentials, and return * an access token for making requests. */ async getAccessToken() { const client = await this.getClient(); return (await client.getAccessToken()).token; } /** * Obtain the HTTP headers that will provide authorization for a given * request. */ async getRequestHeaders(url) { const client = await this.getClient(); return client.getRequestHeaders(url); } /** * Obtain credentials for a request, then attach the appropriate headers to * the request options. * @param opts Axios or Request options on which to attach the headers */ async authorizeRequest(opts) { opts = opts || {}; const url = opts.url || opts.uri; const client = await this.getClient(); const headers = await client.getRequestHeaders(url); opts.headers = Object.assign(opts.headers || {}, headers); return opts; } /** * Automatically obtain application default credentials, and make an * HTTP request using the given options. * @param opts Axios request options for the HTTP request. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any async request(opts) { const client = await this.getClient(); return client.request(opts); } /** * Determine the compute environment in which the code is running. */ getEnv() { return envDetect_1.getEnv(); } /** * Sign the given data with the current private key, or go out * to the IAM API to sign it. * @param data The data to be signed. */ async sign(data) { const client = await this.getClient(); const crypto = crypto_1.createCrypto(); if (client instanceof jwtclient_1.JWT && client.key) { const sign = await crypto.sign(client.key, data); return sign; } // signBlob requires a service account email and the underlying // access token to have iam.serviceAccounts.signBlob permission // on the specified resource name. // The "Service Account Token Creator" role should cover this. // As a result external account credentials can support this // operation when service account impersonation is enabled. if (client instanceof baseexternalclient_1.BaseExternalAccountClient && client.getServiceAccountEmail()) { return this.signBlob(crypto, client.getServiceAccountEmail(), data); } const projectId = await this.getProjectId(); if (!projectId) { throw new Error('Cannot sign data without a project ID.'); } const creds = await this.getCredentials(); if (!creds.client_email) { throw new Error('Cannot sign data without `client_email`.'); } return this.signBlob(crypto, creds.client_email, data); } async signBlob(crypto, emailOrUniqueId, data) { const url = 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/' + `${emailOrUniqueId}:signBlob`; const res = await this.request({ method: 'POST', url, data: { payload: crypto.encodeBase64StringUtf8(data), }, }); return res.data.signedBlob; } } exports.GoogleAuth = GoogleAuth; /** * Export DefaultTransporter as a static property of the class. */ GoogleAuth.DefaultTransporter = transporters_1.DefaultTransporter; //# sourceMappingURL=googleauth.js.map
Back to File Manager