UNPKG

26.5 kBJavaScriptView Raw
1"use strict";
2// Copyright 2019 Google LLC
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15Object.defineProperty(exports, "__esModule", { value: true });
16exports.GoogleAuth = exports.CLOUD_SDK_CLIENT_ID = void 0;
17const child_process_1 = require("child_process");
18const fs = require("fs");
19const gcpMetadata = require("gcp-metadata");
20const os = require("os");
21const path = require("path");
22const crypto_1 = require("../crypto/crypto");
23const transporters_1 = require("../transporters");
24const computeclient_1 = require("./computeclient");
25const idtokenclient_1 = require("./idtokenclient");
26const envDetect_1 = require("./envDetect");
27const jwtclient_1 = require("./jwtclient");
28const refreshclient_1 = require("./refreshclient");
29const externalclient_1 = require("./externalclient");
30const baseexternalclient_1 = require("./baseexternalclient");
31exports.CLOUD_SDK_CLIENT_ID = '764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com';
32class GoogleAuth {
33 constructor(opts) {
34 /**
35 * Caches a value indicating whether the auth layer is running on Google
36 * Compute Engine.
37 * @private
38 */
39 this.checkIsGCE = undefined;
40 // To save the contents of the JSON credential file
41 this.jsonContent = null;
42 this.cachedCredential = null;
43 opts = opts || {};
44 this._cachedProjectId = opts.projectId || null;
45 this.keyFilename = opts.keyFilename || opts.keyFile;
46 this.scopes = opts.scopes;
47 this.jsonContent = opts.credentials || null;
48 this.clientOptions = opts.clientOptions;
49 }
50 // Note: this properly is only public to satisify unit tests.
51 // https://github.com/Microsoft/TypeScript/issues/5228
52 get isGCE() {
53 return this.checkIsGCE;
54 }
55 // GAPIC client libraries should always use self-signed JWTs. The following
56 // variables are set on the JWT client in order to indicate the type of library,
57 // and sign the JWT with the correct audience and scopes (if not supplied).
58 setGapicJWTValues(client) {
59 client.defaultServicePath = this.defaultServicePath;
60 client.useJWTAccessWithScope = this.useJWTAccessWithScope;
61 client.defaultScopes = this.defaultScopes;
62 }
63 getProjectId(callback) {
64 if (callback) {
65 this.getProjectIdAsync().then(r => callback(null, r), callback);
66 }
67 else {
68 return this.getProjectIdAsync();
69 }
70 }
71 getProjectIdAsync() {
72 if (this._cachedProjectId) {
73 return Promise.resolve(this._cachedProjectId);
74 }
75 // In implicit case, supports three environments. In order of precedence,
76 // the implicit environments are:
77 // - GCLOUD_PROJECT or GOOGLE_CLOUD_PROJECT environment variable
78 // - GOOGLE_APPLICATION_CREDENTIALS JSON file
79 // - Cloud SDK: `gcloud config config-helper --format json`
80 // - GCE project ID from metadata server)
81 if (!this._getDefaultProjectIdPromise) {
82 // TODO: refactor the below code so that it doesn't mix and match
83 // promises and async/await.
84 this._getDefaultProjectIdPromise = new Promise(
85 // eslint-disable-next-line no-async-promise-executor
86 async (resolve, reject) => {
87 try {
88 const projectId = this.getProductionProjectId() ||
89 (await this.getFileProjectId()) ||
90 (await this.getDefaultServiceProjectId()) ||
91 (await this.getGCEProjectId()) ||
92 (await this.getExternalAccountClientProjectId());
93 this._cachedProjectId = projectId;
94 if (!projectId) {
95 throw new Error('Unable to detect a Project Id in the current environment. \n' +
96 'To learn more about authentication and Google APIs, visit: \n' +
97 'https://cloud.google.com/docs/authentication/getting-started');
98 }
99 resolve(projectId);
100 }
101 catch (e) {
102 reject(e);
103 }
104 });
105 }
106 return this._getDefaultProjectIdPromise;
107 }
108 /**
109 * @returns Any scopes (user-specified or default scopes specified by the
110 * client library) that need to be set on the current Auth client.
111 */
112 getAnyScopes() {
113 return this.scopes || this.defaultScopes;
114 }
115 getApplicationDefault(optionsOrCallback = {}, callback) {
116 let options;
117 if (typeof optionsOrCallback === 'function') {
118 callback = optionsOrCallback;
119 }
120 else {
121 options = optionsOrCallback;
122 }
123 if (callback) {
124 this.getApplicationDefaultAsync(options).then(r => callback(null, r.credential, r.projectId), callback);
125 }
126 else {
127 return this.getApplicationDefaultAsync(options);
128 }
129 }
130 async getApplicationDefaultAsync(options = {}) {
131 // If we've already got a cached credential, just return it.
132 if (this.cachedCredential) {
133 return {
134 credential: this.cachedCredential,
135 projectId: await this.getProjectIdAsync(),
136 };
137 }
138 let credential;
139 let projectId;
140 // Check for the existence of a local environment variable pointing to the
141 // location of the credential file. This is typically used in local
142 // developer scenarios.
143 credential =
144 await this._tryGetApplicationCredentialsFromEnvironmentVariable(options);
145 if (credential) {
146 if (credential instanceof jwtclient_1.JWT) {
147 credential.scopes = this.scopes;
148 }
149 else if (credential instanceof baseexternalclient_1.BaseExternalAccountClient) {
150 credential.scopes = this.getAnyScopes();
151 }
152 this.cachedCredential = credential;
153 projectId = await this.getProjectId();
154 return { credential, projectId };
155 }
156 // Look in the well-known credential file location.
157 credential = await this._tryGetApplicationCredentialsFromWellKnownFile(options);
158 if (credential) {
159 if (credential instanceof jwtclient_1.JWT) {
160 credential.scopes = this.scopes;
161 }
162 else if (credential instanceof baseexternalclient_1.BaseExternalAccountClient) {
163 credential.scopes = this.getAnyScopes();
164 }
165 this.cachedCredential = credential;
166 projectId = await this.getProjectId();
167 return { credential, projectId };
168 }
169 // Determine if we're running on GCE.
170 let isGCE;
171 try {
172 isGCE = await this._checkIsGCE();
173 }
174 catch (e) {
175 e.message = `Unexpected error determining execution environment: ${e.message}`;
176 throw e;
177 }
178 if (!isGCE) {
179 // We failed to find the default credentials. Bail out with an error.
180 throw new Error('Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.');
181 }
182 // For GCE, just return a default ComputeClient. It will take care of
183 // the rest.
184 options.scopes = this.getAnyScopes();
185 this.cachedCredential = new computeclient_1.Compute(options);
186 projectId = await this.getProjectId();
187 return { projectId, credential: this.cachedCredential };
188 }
189 /**
190 * Determines whether the auth layer is running on Google Compute Engine.
191 * @returns A promise that resolves with the boolean.
192 * @api private
193 */
194 async _checkIsGCE() {
195 if (this.checkIsGCE === undefined) {
196 this.checkIsGCE = await gcpMetadata.isAvailable();
197 }
198 return this.checkIsGCE;
199 }
200 /**
201 * Attempts to load default credentials from the environment variable path..
202 * @returns Promise that resolves with the OAuth2Client or null.
203 * @api private
204 */
205 async _tryGetApplicationCredentialsFromEnvironmentVariable(options) {
206 const credentialsPath = process.env['GOOGLE_APPLICATION_CREDENTIALS'] ||
207 process.env['google_application_credentials'];
208 if (!credentialsPath || credentialsPath.length === 0) {
209 return null;
210 }
211 try {
212 return this._getApplicationCredentialsFromFilePath(credentialsPath, options);
213 }
214 catch (e) {
215 e.message = `Unable to read the credential file specified by the GOOGLE_APPLICATION_CREDENTIALS environment variable: ${e.message}`;
216 throw e;
217 }
218 }
219 /**
220 * Attempts to load default credentials from a well-known file location
221 * @return Promise that resolves with the OAuth2Client or null.
222 * @api private
223 */
224 async _tryGetApplicationCredentialsFromWellKnownFile(options) {
225 // First, figure out the location of the file, depending upon the OS type.
226 let location = null;
227 if (this._isWindows()) {
228 // Windows
229 location = process.env['APPDATA'];
230 }
231 else {
232 // Linux or Mac
233 const home = process.env['HOME'];
234 if (home) {
235 location = path.join(home, '.config');
236 }
237 }
238 // If we found the root path, expand it.
239 if (location) {
240 location = path.join(location, 'gcloud', 'application_default_credentials.json');
241 if (!fs.existsSync(location)) {
242 location = null;
243 }
244 }
245 // The file does not exist.
246 if (!location) {
247 return null;
248 }
249 // The file seems to exist. Try to use it.
250 const client = await this._getApplicationCredentialsFromFilePath(location, options);
251 return client;
252 }
253 /**
254 * Attempts to load default credentials from a file at the given path..
255 * @param filePath The path to the file to read.
256 * @returns Promise that resolves with the OAuth2Client
257 * @api private
258 */
259 async _getApplicationCredentialsFromFilePath(filePath, options = {}) {
260 // Make sure the path looks like a string.
261 if (!filePath || filePath.length === 0) {
262 throw new Error('The file path is invalid.');
263 }
264 // Make sure there is a file at the path. lstatSync will throw if there is
265 // nothing there.
266 try {
267 // Resolve path to actual file in case of symlink. Expect a thrown error
268 // if not resolvable.
269 filePath = fs.realpathSync(filePath);
270 if (!fs.lstatSync(filePath).isFile()) {
271 throw new Error();
272 }
273 }
274 catch (err) {
275 err.message = `The file at ${filePath} does not exist, or it is not a file. ${err.message}`;
276 throw err;
277 }
278 // Now open a read stream on the file, and parse it.
279 const readStream = fs.createReadStream(filePath);
280 return this.fromStream(readStream, options);
281 }
282 /**
283 * Create a credentials instance using the given input options.
284 * @param json The input object.
285 * @param options The JWT or UserRefresh options for the client
286 * @returns JWT or UserRefresh Client with data
287 */
288 fromJSON(json, options) {
289 let client;
290 if (!json) {
291 throw new Error('Must pass in a JSON object containing the Google auth settings.');
292 }
293 options = options || {};
294 if (json.type === 'authorized_user') {
295 client = new refreshclient_1.UserRefreshClient(options);
296 client.fromJSON(json);
297 }
298 else if (json.type === baseexternalclient_1.EXTERNAL_ACCOUNT_TYPE) {
299 client = externalclient_1.ExternalAccountClient.fromJSON(json, options);
300 client.scopes = this.getAnyScopes();
301 }
302 else {
303 options.scopes = this.scopes;
304 client = new jwtclient_1.JWT(options);
305 this.setGapicJWTValues(client);
306 client.fromJSON(json);
307 }
308 return client;
309 }
310 /**
311 * Return a JWT or UserRefreshClient from JavaScript object, caching both the
312 * object used to instantiate and the client.
313 * @param json The input object.
314 * @param options The JWT or UserRefresh options for the client
315 * @returns JWT or UserRefresh Client with data
316 */
317 _cacheClientFromJSON(json, options) {
318 let client;
319 // create either a UserRefreshClient or JWT client.
320 options = options || {};
321 if (json.type === 'authorized_user') {
322 client = new refreshclient_1.UserRefreshClient(options);
323 client.fromJSON(json);
324 }
325 else if (json.type === baseexternalclient_1.EXTERNAL_ACCOUNT_TYPE) {
326 client = externalclient_1.ExternalAccountClient.fromJSON(json, options);
327 client.scopes = this.getAnyScopes();
328 }
329 else {
330 options.scopes = this.scopes;
331 client = new jwtclient_1.JWT(options);
332 this.setGapicJWTValues(client);
333 client.fromJSON(json);
334 }
335 // cache both raw data used to instantiate client and client itself.
336 this.jsonContent = json;
337 this.cachedCredential = client;
338 return this.cachedCredential;
339 }
340 fromStream(inputStream, optionsOrCallback = {}, callback) {
341 let options = {};
342 if (typeof optionsOrCallback === 'function') {
343 callback = optionsOrCallback;
344 }
345 else {
346 options = optionsOrCallback;
347 }
348 if (callback) {
349 this.fromStreamAsync(inputStream, options).then(r => callback(null, r), callback);
350 }
351 else {
352 return this.fromStreamAsync(inputStream, options);
353 }
354 }
355 fromStreamAsync(inputStream, options) {
356 return new Promise((resolve, reject) => {
357 if (!inputStream) {
358 throw new Error('Must pass in a stream containing the Google auth settings.');
359 }
360 let s = '';
361 inputStream
362 .setEncoding('utf8')
363 .on('error', reject)
364 .on('data', chunk => (s += chunk))
365 .on('end', () => {
366 try {
367 try {
368 const data = JSON.parse(s);
369 const r = this._cacheClientFromJSON(data, options);
370 return resolve(r);
371 }
372 catch (err) {
373 // If we failed parsing this.keyFileName, assume that it
374 // is a PEM or p12 certificate:
375 if (!this.keyFilename)
376 throw err;
377 const client = new jwtclient_1.JWT({
378 ...this.clientOptions,
379 keyFile: this.keyFilename,
380 });
381 this.cachedCredential = client;
382 this.setGapicJWTValues(client);
383 return resolve(client);
384 }
385 }
386 catch (err) {
387 return reject(err);
388 }
389 });
390 });
391 }
392 /**
393 * Create a credentials instance using the given API key string.
394 * @param apiKey The API key string
395 * @param options An optional options object.
396 * @returns A JWT loaded from the key
397 */
398 fromAPIKey(apiKey, options) {
399 options = options || {};
400 const client = new jwtclient_1.JWT(options);
401 client.fromAPIKey(apiKey);
402 return client;
403 }
404 /**
405 * Determines whether the current operating system is Windows.
406 * @api private
407 */
408 _isWindows() {
409 const sys = os.platform();
410 if (sys && sys.length >= 3) {
411 if (sys.substring(0, 3).toLowerCase() === 'win') {
412 return true;
413 }
414 }
415 return false;
416 }
417 /**
418 * Run the Google Cloud SDK command that prints the default project ID
419 */
420 async getDefaultServiceProjectId() {
421 return new Promise(resolve => {
422 child_process_1.exec('gcloud config config-helper --format json', (err, stdout) => {
423 if (!err && stdout) {
424 try {
425 const projectId = JSON.parse(stdout).configuration.properties.core.project;
426 resolve(projectId);
427 return;
428 }
429 catch (e) {
430 // ignore errors
431 }
432 }
433 resolve(null);
434 });
435 });
436 }
437 /**
438 * Loads the project id from environment variables.
439 * @api private
440 */
441 getProductionProjectId() {
442 return (process.env['GCLOUD_PROJECT'] ||
443 process.env['GOOGLE_CLOUD_PROJECT'] ||
444 process.env['gcloud_project'] ||
445 process.env['google_cloud_project']);
446 }
447 /**
448 * Loads the project id from the GOOGLE_APPLICATION_CREDENTIALS json file.
449 * @api private
450 */
451 async getFileProjectId() {
452 if (this.cachedCredential) {
453 // Try to read the project ID from the cached credentials file
454 return this.cachedCredential.projectId;
455 }
456 // Ensure the projectId is loaded from the keyFile if available.
457 if (this.keyFilename) {
458 const creds = await this.getClient();
459 if (creds && creds.projectId) {
460 return creds.projectId;
461 }
462 }
463 // Try to load a credentials file and read its project ID
464 const r = await this._tryGetApplicationCredentialsFromEnvironmentVariable();
465 if (r) {
466 return r.projectId;
467 }
468 else {
469 return null;
470 }
471 }
472 /**
473 * Gets the project ID from external account client if available.
474 */
475 async getExternalAccountClientProjectId() {
476 if (!this.jsonContent || this.jsonContent.type !== baseexternalclient_1.EXTERNAL_ACCOUNT_TYPE) {
477 return null;
478 }
479 const creds = await this.getClient();
480 // Do not suppress the underlying error, as the error could contain helpful
481 // information for debugging and fixing. This is especially true for
482 // external account creds as in order to get the project ID, the following
483 // operations have to succeed:
484 // 1. Valid credentials file should be supplied.
485 // 2. Ability to retrieve access tokens from STS token exchange API.
486 // 3. Ability to exchange for service account impersonated credentials (if
487 // enabled).
488 // 4. Ability to get project info using the access token from step 2 or 3.
489 // Without surfacing the error, it is harder for developers to determine
490 // which step went wrong.
491 return await creds.getProjectId();
492 }
493 /**
494 * Gets the Compute Engine project ID if it can be inferred.
495 */
496 async getGCEProjectId() {
497 try {
498 const r = await gcpMetadata.project('project-id');
499 return r;
500 }
501 catch (e) {
502 // Ignore any errors
503 return null;
504 }
505 }
506 getCredentials(callback) {
507 if (callback) {
508 this.getCredentialsAsync().then(r => callback(null, r), callback);
509 }
510 else {
511 return this.getCredentialsAsync();
512 }
513 }
514 async getCredentialsAsync() {
515 await this.getClient();
516 if (this.jsonContent) {
517 const credential = {
518 client_email: this.jsonContent.client_email,
519 private_key: this.jsonContent.private_key,
520 };
521 return credential;
522 }
523 const isGCE = await this._checkIsGCE();
524 if (!isGCE) {
525 throw new Error('Unknown error.');
526 }
527 // For GCE, return the service account details from the metadata server
528 // NOTE: The trailing '/' at the end of service-accounts/ is very important!
529 // The GCF metadata server doesn't respect querystring params if this / is
530 // not included.
531 const data = await gcpMetadata.instance({
532 property: 'service-accounts/',
533 params: { recursive: 'true' },
534 });
535 if (!data || !data.default || !data.default.email) {
536 throw new Error('Failure from metadata server.');
537 }
538 return { client_email: data.default.email };
539 }
540 /**
541 * Automatically obtain a client based on the provided configuration. If no
542 * options were passed, use Application Default Credentials.
543 */
544 async getClient(options) {
545 if (options) {
546 throw new Error('Passing options to getClient is forbidden in v5.0.0. Use new GoogleAuth(opts) instead.');
547 }
548 if (!this.cachedCredential) {
549 if (this.jsonContent) {
550 this._cacheClientFromJSON(this.jsonContent, this.clientOptions);
551 }
552 else if (this.keyFilename) {
553 const filePath = path.resolve(this.keyFilename);
554 const stream = fs.createReadStream(filePath);
555 await this.fromStreamAsync(stream, this.clientOptions);
556 }
557 else {
558 await this.getApplicationDefaultAsync(this.clientOptions);
559 }
560 }
561 return this.cachedCredential;
562 }
563 /**
564 * Creates a client which will fetch an ID token for authorization.
565 * @param targetAudience the audience for the fetched ID token.
566 * @returns IdTokenClient for making HTTP calls authenticated with ID tokens.
567 */
568 async getIdTokenClient(targetAudience) {
569 const client = await this.getClient();
570 if (!('fetchIdToken' in client)) {
571 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.');
572 }
573 return new idtokenclient_1.IdTokenClient({ targetAudience, idTokenProvider: client });
574 }
575 /**
576 * Automatically obtain application default credentials, and return
577 * an access token for making requests.
578 */
579 async getAccessToken() {
580 const client = await this.getClient();
581 return (await client.getAccessToken()).token;
582 }
583 /**
584 * Obtain the HTTP headers that will provide authorization for a given
585 * request.
586 */
587 async getRequestHeaders(url) {
588 const client = await this.getClient();
589 return client.getRequestHeaders(url);
590 }
591 /**
592 * Obtain credentials for a request, then attach the appropriate headers to
593 * the request options.
594 * @param opts Axios or Request options on which to attach the headers
595 */
596 async authorizeRequest(opts) {
597 opts = opts || {};
598 const url = opts.url || opts.uri;
599 const client = await this.getClient();
600 const headers = await client.getRequestHeaders(url);
601 opts.headers = Object.assign(opts.headers || {}, headers);
602 return opts;
603 }
604 /**
605 * Automatically obtain application default credentials, and make an
606 * HTTP request using the given options.
607 * @param opts Axios request options for the HTTP request.
608 */
609 // eslint-disable-next-line @typescript-eslint/no-explicit-any
610 async request(opts) {
611 const client = await this.getClient();
612 return client.request(opts);
613 }
614 /**
615 * Determine the compute environment in which the code is running.
616 */
617 getEnv() {
618 return envDetect_1.getEnv();
619 }
620 /**
621 * Sign the given data with the current private key, or go out
622 * to the IAM API to sign it.
623 * @param data The data to be signed.
624 */
625 async sign(data) {
626 const client = await this.getClient();
627 const crypto = crypto_1.createCrypto();
628 if (client instanceof jwtclient_1.JWT && client.key) {
629 const sign = await crypto.sign(client.key, data);
630 return sign;
631 }
632 // signBlob requires a service account email and the underlying
633 // access token to have iam.serviceAccounts.signBlob permission
634 // on the specified resource name.
635 // The "Service Account Token Creator" role should cover this.
636 // As a result external account credentials can support this
637 // operation when service account impersonation is enabled.
638 if (client instanceof baseexternalclient_1.BaseExternalAccountClient &&
639 client.getServiceAccountEmail()) {
640 return this.signBlob(crypto, client.getServiceAccountEmail(), data);
641 }
642 const projectId = await this.getProjectId();
643 if (!projectId) {
644 throw new Error('Cannot sign data without a project ID.');
645 }
646 const creds = await this.getCredentials();
647 if (!creds.client_email) {
648 throw new Error('Cannot sign data without `client_email`.');
649 }
650 return this.signBlob(crypto, creds.client_email, data);
651 }
652 async signBlob(crypto, emailOrUniqueId, data) {
653 const url = 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/' +
654 `${emailOrUniqueId}:signBlob`;
655 const res = await this.request({
656 method: 'POST',
657 url,
658 data: {
659 payload: crypto.encodeBase64StringUtf8(data),
660 },
661 });
662 return res.data.signedBlob;
663 }
664}
665exports.GoogleAuth = GoogleAuth;
666/**
667 * Export DefaultTransporter as a static property of the class.
668 */
669GoogleAuth.DefaultTransporter = transporters_1.DefaultTransporter;
670//# sourceMappingURL=googleauth.js.map
\No newline at end of file